1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */
2 /*
3  * This file is part of GtkSourceView
4  *
5  * Copyright (C) 2013-2016 - Sébastien Wilmet <swilmet@gnome.org>
6  *
7  * GtkSourceView is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * GtkSourceView is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public License
18  * along with this library; if not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #ifdef HAVE_CONFIG_H
22 #include <config.h>
23 #endif
24 
25 #include "gtksourcesearchcontext.h"
26 #include <string.h>
27 #include "gtksourcesearchsettings.h"
28 #include "gtksourcebuffer.h"
29 #include "gtksourcebuffer-private.h"
30 #include "gtksourcebufferinternal.h"
31 #include "gtksourcestyle.h"
32 #include "gtksourcestylescheme.h"
33 #include "gtksourceutils.h"
34 #include "gtksourceregion.h"
35 #include "gtksourceiter.h"
36 #include "gtksource-enumtypes.h"
37 
38 /**
39  * SECTION:searchcontext
40  * @Short_description: Search context
41  * @Title: GtkSourceSearchContext
42  * @See_also: #GtkSourceBuffer, #GtkSourceSearchSettings
43  *
44  * A #GtkSourceSearchContext is used for the search and replace in a
45  * #GtkSourceBuffer. The search settings are represented by a
46  * #GtkSourceSearchSettings object. There can be a many-to-many relationship
47  * between buffers and search settings, with the search contexts in-between: a
48  * search settings object can be shared between several search contexts; and a
49  * buffer can contain several search contexts at the same time.
50  *
51  * The total number of search occurrences can be retrieved with
52  * gtk_source_search_context_get_occurrences_count(). To know the position of a
53  * certain match, use gtk_source_search_context_get_occurrence_position().
54  *
55  * The buffer is scanned asynchronously, so it doesn't block the user interface.
56  * For each search, the buffer is scanned at most once. After that, navigating
57  * through the occurrences doesn't require to re-scan the buffer entirely.
58  *
59  * To search forward, use gtk_source_search_context_forward() or
60  * gtk_source_search_context_forward_async() for the asynchronous version.
61  * The backward search is done similarly. To replace a search match, or all
62  * matches, use gtk_source_search_context_replace() and
63  * gtk_source_search_context_replace_all().
64  *
65  * The search occurrences are highlighted by default. To disable it, use
66  * gtk_source_search_context_set_highlight(). You can enable the search
67  * highlighting for several #GtkSourceSearchContext<!-- -->s attached to the
68  * same buffer. Moreover, each of those #GtkSourceSearchContext<!-- -->s can
69  * have a different text style associated. Use
70  * gtk_source_search_context_set_match_style() to specify the #GtkSourceStyle
71  * to apply on search matches.
72  *
73  * Note that the #GtkSourceSearchContext:highlight and
74  * #GtkSourceSearchContext:match-style properties are in the
75  * #GtkSourceSearchContext class, not #GtkSourceSearchSettings. Appearance
76  * settings should be tied to one, and only one buffer, as different buffers can
77  * have different style scheme associated (a #GtkSourceSearchSettings object
78  * can be bound indirectly to several buffers).
79  *
80  * The concept of "current match" doesn't exist yet. A way to highlight
81  * differently the current match is to select it.
82  *
83  * A search occurrence's position doesn't depend on the cursor position or other
84  * parameters. Take for instance the buffer "aaaa" with the search text "aa".
85  * The two occurrences are at positions [0:2] and [2:4]. If you begin to search
86  * at position 1, you will get the occurrence [2:4], not [1:3]. This is a
87  * prerequisite for regular expression searches. The pattern ".*" matches the
88  * entire line. If the cursor is at the middle of the line, you don't want the
89  * rest of the line as the occurrence, you want an entire line. (As a side note,
90  * regular expression searches can also match multiple lines.)
91  *
92  * In the GtkSourceView source code, there is an example of how to use the
93  * search and replace API: see the tests/test-search.c file. It is a mini
94  * application for the search and replace, with a basic user interface.
95  */
96 
97 /* Implementation overview:
98  *
99  * When the state of the search changes (the text to search or the options), we
100  * have to update the highlighting and the properties values (the number of
101  * occurrences). To do so, a simple solution is to first remove all the
102  * found_tag, so we have a clean buffer to analyze. The problem with this
103  * solution is that there is some flickering when the user modifies the text to
104  * search, because removing all the found_tag's can take some time. So we keep
105  * the old found_tag's, and when we must highlight the matches in a certain
106  * region, we first remove the found_tag's in this region and then we highlight
107  * the newly found matches by applying the found_tag to them.
108  *
109  * If we only want to highlight the matches, without counting the number of
110  * occurrences, a good solution would be to highlight only the visible region of
111  * the buffer on the screen. So it would be useless to always scan all the
112  * buffer.
113  *
114  * But we actually want the number of occurrences! So we have to scan all the
115  * buffer. When the state of the search changes, an idle callback is installed,
116  * which will scan the buffer to highlight the matches. To avoid flickering, the
117  * visible region on the screen is put in a higher priority region to highlight,
118  * so the idle callback will first scan this region.
119  *
120  * Why highlighting the non-visible matches? What we want is to (1) highlight
121  * the visible matches and (2) count the number of occurrences. The code would
122  * indeed be simpler if these two tasks were clearly separated (in two different
123  * idle callbacks, with different regions to scan). With this simpler solution,
124  * we would always use forward_search() and backward_search() to navigate
125  * through the occurrences. But we can do better than that!
126  * forward_to_tag_toggle() and backward_to_tag_toggle() are far more efficient:
127  * once the buffer has been scanned, going to the previous or the next
128  * occurrence is done in O(log n), with n the length of the buffer. We must just
129  * pay attention to contiguous matches.
130  *
131  * While the user is typing the text in the search entry, the buffer is scanned
132  * to count the number of occurrences. And when the user wants to do an
133  * operation (go to the next occurrence for example), chances are that the
134  * buffer has already been scanned entirely, so almost all the operations will
135  * be really fast.
136  *
137  * Extreme example:
138  * <occurrence> [1 GB of text] <next-occurrence>
139  * Once the buffer is scanned, switching between the occurrences will be almost
140  * instantaneous.
141  *
142  * So how to count the number of occurrences then? Remember that the buffer
143  * contents can be modified during the scan, and that we keep the old
144  * found_tag's. Moreover, when we encounter an old found_tag region, in the
145  * general case we can not say how many occurrences there are in this region,
146  * since a found_tag region can contain contiguous matches. Take for example the
147  * found_tag region "aa": was it the "aa" search match, or two times "a"?
148  * The implemented solution is to set occurrences_count to 0 when the search
149  * state changes, even if old matches are still there. Because it is not
150  * possible to count the old matches to decrement occurrences_count (and storing
151  * the previous search text would not be sufficient, because even older matches
152  * can still be there). To increment and decrement occurrences_count, there is
153  * the scan_region, the region to scan. If an occurrence is contained in
154  * scan_region, it means that it has not already been scanned, so
155  * occurrences_count doesn't take into account this occurrence. On the other
156  * hand, if we find an occurrence outside scan_region, the occurrence is
157  * normally correctly highlighted, and occurrences_count take it into account.
158  *
159  * So when we highlight or when we remove the highlight of an occurrence (on
160  * text insertion, deletion, when scanning, etc.), we increment or decrement
161  * occurrences_count depending on whether the occurrence was already taken into
162  * account by occurrences_count.
163  *
164  * If the code seems too complicated and contains strange bugs, you have two
165  * choices:
166  * - Write more unit tests, understand correctly the code and fix it.
167  * - Rewrite the code to implement the simpler solution explained above :-)
168  *   But with the simpler solution, multiple-lines regex search matches (see
169  *   below) would not be possible for going to the previous occurrence (or the
170  *   buffer must always be scanned from the beginning).
171  *
172  * Known issue
173  * -----------
174  *
175  * Contiguous matches have a performance problem. In some cases it can lead to
176  * an O(N^2) time complexity. For example if the buffer is full of contiguous
177  * matches, and we want to navigate through all of them. If an iter is in the
178  * middle of a found_tag region, we don't know where are the nearest occurrence
179  * boundaries. Take for example the buffer "aaaa" with the search text "aa". The
180  * two occurrences are at positions [0:2] and [2:4]. If we begin to search at
181  * position 1, we can not take [1:3] as an occurrence. So what the code do is to
182  * go backward to the start of the found_tag region, and do a basic search
183  * inside the found_tag region to find the occurrence boundaries.
184  *
185  * So this is really a corner case, but it's better to be aware of that.
186  * To fix the problem, one solution would be to have two found_tag, and
187  * alternate them for contiguous matches.
188  */
189 
190 /* Regex search:
191  *
192  * With a regex, we don't know how many lines a match can span. A regex will
193  * most probably match only one line, but a regex can contain something like
194  * "\n*", or the dot metacharacter can also match newlines, with the "?s" option
195  * (see G_REGEX_DOTALL).
196  * Therefore a simple solution is to always begin the search at the beginning of
197  * the document. Only the scan_region is taken into account for scanning the
198  * buffer.
199  *
200  * For non-regex searches, when there is an insertion or deletion in the buffer,
201  * we don't need to re-scan all the buffer. If there is an unmodified match in
202  * the neighborhood, no need to re-scan it (unless at_word_boundaries is set).
203  * For a regex search, it is more complicated. An insertion or deletion outside
204  * a match can modify a match located in the neighborhood. Take for example the
205  * regex "(aa)+" with the buffer contents "aaa". There is one occurrence: the
206  * first two letters. If we insert an extra 'a' at the end of the buffer, the
207  * occurrence is modified to take the next two letters. That's why the buffer
208  * is re-scanned entirely on each insertion or deletion in the buffer.
209  *
210  * For searching the matches, the easiest solution is to retrieve all the buffer
211  * contents, and search the occurrences on this big string. But it takes a lot
212  * of memory space. It is better to do multi-segment matching, also called
213  * incremental matching. See the pcrepartial(3) manpage. The matching is done
214  * segment by segment, with the G_REGEX_MATCH_PARTIAL_HARD flag (for reasons
215  * explained in the manpage). We begin by the first segment of the buffer as the
216  * subject string. If a partial match is returned, we append the next segment to
217  * the subject string, and we try again to find a complete match. When a
218  * complete match is returned, we must continue to search the next occurrences.
219  * The max lookbehind of the pattern must be retrieved. The start of the next
220  * subject string is located at max_lookbehind characters before the end of the
221  * previously found match. Similarly, if no match is found (neither a complete
222  * match nor a partial match), we take the next segment, with the last
223  * max_lookbehind characters from the previous segment.
224  *
225  * Improvement idea
226  * ----------------
227  *
228  * What we would like to support in applications is the incremental search:
229  * while we type the pattern, the buffer is scanned and the matches are
230  * highlighted. When the pattern is not fully typed, strange things can happen,
231  * including a pattern that match the entire buffer. And if the user is
232  * working on a really big file, catastrophe: the UI is blocked!
233  * To avoid this problem, a solution is to search the buffer differently
234  * depending on the situation:
235  * - First situation: the subject string to scan is small enough, we retrieve it
236  *   and scan it directly.
237  * - Second situation: the subject string to scan is too big, it will take
238  *   too much time to retrieve it and scan it directly. We handle this situation
239  *   in three phases: (1) retrieving the subject string, chunks by chunks, in
240  *   several idle loop iterations. (2) Once the subject string is retrieved
241  *   completely, we launch the regex matching in a thread. (3) Once the thread
242  *   is finished, we highlight the matches in the buffer. And voilà.
243  *
244  * But in practice, when trying a pattern that match the entire buffer, we
245  * quickly get an error like:
246  *
247  * 	Regex matching error: Error while matching regular expression (.*\n)*:
248  * 	recursion limit reached
249  *
250  * It happens with test-search, with this file as the buffer (~3500 lines).
251  *
252  * Improvement idea
253  * ----------------
254  *
255  * GRegex currently doesn't support JIT pattern compilation:
256  * https://bugzilla.gnome.org/show_bug.cgi?id=679155
257  *
258  * Once available, it can be a nice performance improvement (but it must be
259  * measured, since g_regex_new() is slower with JIT enabled).
260  *
261  * Known issue
262  * -----------
263  *
264  * To search at word boundaries, \b is added at the beginning and at the
265  * end of the pattern. But \b is not the same as
266  * _gtk_source_iter_starts_extra_natural_word() and
267  * _gtk_source_iter_ends_extra_natural_word(). \b for
268  * example doesn't take the underscore as a word boundary.
269  * Using _gtk_source_iter_starts_extra_natural_word() and ends_word() for regex searches
270  * is not easily possible: if the GRegex returns a match, but doesn't
271  * start and end a word, maybe a shorter match (for a greedy pattern)
272  * start and end a word, or a longer match (for an ungreedy pattern). To
273  * be able to use the _gtk_source_iter_starts_extra_natural_word() and ends_word()
274  * functions for regex search, g_regex_match_all_full() must be used, to
275  * retrieve _all_ matches, and test the word boundaries until a match is
276  * found at word boundaries.
277  */
278 
279 /*
280 #define ENABLE_DEBUG
281 */
282 #undef ENABLE_DEBUG
283 
284 #ifdef ENABLE_DEBUG
285 #define DEBUG(x) (x)
286 #else
287 #define DEBUG(x)
288 #endif
289 
290 /* Maximum number of lines to scan in one batch.
291  * A lower value means more overhead when scanning the buffer asynchronously.
292  */
293 #define SCAN_BATCH_SIZE 100
294 
295 enum
296 {
297 	PROP_0,
298 	PROP_BUFFER,
299 	PROP_SETTINGS,
300 	PROP_HIGHLIGHT,
301 	PROP_MATCH_STYLE,
302 	PROP_OCCURRENCES_COUNT,
303 	PROP_REGEX_ERROR
304 };
305 
306 struct _GtkSourceSearchContextPrivate
307 {
308 	/* Weak ref to the buffer. The buffer has also a weak ref to the search
309 	 * context. A strong ref in either direction would prevent the pointed
310 	 * object to be finalized.
311 	 */
312 	GtkTextBuffer *buffer;
313 
314 	GtkSourceSearchSettings *settings;
315 
316 	/* The tag to apply to search occurrences. Even if the highlighting is
317 	 * disabled, the tag is applied.
318 	 */
319 	GtkTextTag *found_tag;
320 
321 	/* A reference to the tag table where the found_tag is added. The sole
322 	 * purpose is to remove the found_tag in dispose(). We can not rely on
323 	 * 'buffer' since it is a weak reference.
324 	 */
325 	GtkTextTagTable *tag_table;
326 
327 	/* The region to scan and highlight. If NULL, the scan is finished. */
328 	GtkSourceRegion *scan_region;
329 
330 	/* The region to scan and highlight in priority. I.e. the visible part
331 	 * of the buffer on the screen.
332 	 */
333 	GtkSourceRegion *high_priority_region;
334 
335 	/* An asynchronous running task. task_region has a higher priority than
336 	 * scan_region, but a lower priority than high_priority_region.
337 	 */
338 	GTask *task;
339 	GtkSourceRegion *task_region;
340 
341 	/* If the regex search is disabled, text_nb_lines is the number of lines
342 	 * of the search text. It is useful to adjust the region to scan.
343 	 */
344 	gint text_nb_lines;
345 
346 	GRegex *regex;
347 	GError *regex_error;
348 
349 	gint occurrences_count;
350 	gulong idle_scan_id;
351 
352 	GtkSourceStyle *match_style;
353 	guint highlight : 1;
354 };
355 
356 /* Data for the asynchronous forward and backward search tasks. */
357 typedef struct
358 {
359 	GtkTextMark *start_at;
360 	GtkTextMark *match_start;
361 	GtkTextMark *match_end;
362 	guint found : 1;
363 	guint wrapped_around : 1;
364 
365 	/* forward or backward */
366 	guint is_forward : 1;
367 } ForwardBackwardData;
368 
369 G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceSearchContext, gtk_source_search_context, G_TYPE_OBJECT);
370 
371 static void		install_idle_scan		(GtkSourceSearchContext *search);
372 
373 #ifdef ENABLE_DEBUG
374 static void
print_region(GtkSourceRegion * region)375 print_region (GtkSourceRegion *region)
376 {
377 	gchar *str;
378 
379 	str = gtk_source_region_to_string (region);
380 	g_print ("%s\n", str);
381 	g_free (str);
382 }
383 #endif
384 
385 static void
sync_found_tag(GtkSourceSearchContext * search)386 sync_found_tag (GtkSourceSearchContext *search)
387 {
388 	GtkSourceStyle *style = search->priv->match_style;
389 	GtkSourceStyleScheme *style_scheme;
390 
391 	if (search->priv->buffer == NULL)
392 	{
393 		return;
394 	}
395 
396 	if (!search->priv->highlight)
397 	{
398 		gtk_source_style_apply (NULL, search->priv->found_tag);
399 		return;
400 	}
401 
402 	if (style == NULL)
403 	{
404 		style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (search->priv->buffer));
405 
406 		if (style_scheme != NULL)
407 		{
408 			style = gtk_source_style_scheme_get_style (style_scheme, "search-match");
409 		}
410 	}
411 
412 	if (style == NULL)
413 	{
414 		g_warning ("No match style defined nor 'search-match' style available.");
415 	}
416 
417 	gtk_source_style_apply (style, search->priv->found_tag);
418 }
419 
420 static void
text_tag_set_highest_priority(GtkTextTag * tag,GtkTextBuffer * buffer)421 text_tag_set_highest_priority (GtkTextTag    *tag,
422 			       GtkTextBuffer *buffer)
423 {
424 	GtkTextTagTable *table;
425 	gint n;
426 
427 	table = gtk_text_buffer_get_tag_table (buffer);
428 	n = gtk_text_tag_table_get_size (table);
429 	gtk_text_tag_set_priority (tag, n - 1);
430 }
431 
432 /* Sets @start and @end to the first non-empty subregion.
433  * Returns FALSE if the region is empty.
434  */
435 static gboolean
get_first_subregion(GtkSourceRegion * region,GtkTextIter * start,GtkTextIter * end)436 get_first_subregion (GtkSourceRegion *region,
437 		     GtkTextIter     *start,
438 		     GtkTextIter     *end)
439 {
440 	GtkSourceRegionIter region_iter;
441 
442 	if (region == NULL)
443 	{
444 		return FALSE;
445 	}
446 
447 	gtk_source_region_get_start_region_iter (region, &region_iter);
448 
449 	while (!gtk_source_region_iter_is_end (&region_iter))
450 	{
451 		if (!gtk_source_region_iter_get_subregion (&region_iter, start, end))
452 		{
453 			return FALSE;
454 		}
455 
456 		if (!gtk_text_iter_equal (start, end))
457 		{
458 			return TRUE;
459 		}
460 
461 		gtk_source_region_iter_next (&region_iter);
462 	}
463 
464 	return FALSE;
465 }
466 
467 /* Sets @start and @end to the last non-empty subregion.
468  * Returns FALSE if the region is empty.
469  */
470 static gboolean
get_last_subregion(GtkSourceRegion * region,GtkTextIter * start,GtkTextIter * end)471 get_last_subregion (GtkSourceRegion *region,
472 		    GtkTextIter     *start,
473 		    GtkTextIter     *end)
474 {
475 	GtkSourceRegionIter region_iter;
476 	gboolean found = FALSE;
477 
478 	if (region == NULL)
479 	{
480 		return FALSE;
481 	}
482 
483 	gtk_source_region_get_start_region_iter (region, &region_iter);
484 
485 	while (!gtk_source_region_iter_is_end (&region_iter))
486 	{
487 		GtkTextIter start_subregion;
488 		GtkTextIter end_subregion;
489 
490 		if (!gtk_source_region_iter_get_subregion (&region_iter,
491 							   &start_subregion,
492 							   &end_subregion))
493 		{
494 			return FALSE;
495 		}
496 
497 		if (!gtk_text_iter_equal (&start_subregion, &end_subregion))
498 		{
499 			found = TRUE;
500 			*start = start_subregion;
501 			*end = end_subregion;
502 		}
503 
504 		gtk_source_region_iter_next (&region_iter);
505 	}
506 
507 	return found;
508 }
509 
510 static void
clear_task(GtkSourceSearchContext * search)511 clear_task (GtkSourceSearchContext *search)
512 {
513 	g_clear_object (&search->priv->task_region);
514 
515 	if (search->priv->task != NULL)
516 	{
517 		GCancellable *cancellable = g_task_get_cancellable (search->priv->task);
518 
519 		if (cancellable != NULL)
520 		{
521 			g_cancellable_cancel (cancellable);
522 			g_task_return_error_if_cancelled (search->priv->task);
523 		}
524 
525 		g_clear_object (&search->priv->task);
526 	}
527 }
528 
529 static void
clear_search(GtkSourceSearchContext * search)530 clear_search (GtkSourceSearchContext *search)
531 {
532 	g_clear_object (&search->priv->scan_region);
533 	g_clear_object (&search->priv->high_priority_region);
534 
535 	if (search->priv->idle_scan_id != 0)
536 	{
537 		g_source_remove (search->priv->idle_scan_id);
538 		search->priv->idle_scan_id = 0;
539 	}
540 
541 	if (search->priv->regex_error != NULL)
542 	{
543 		g_clear_error (&search->priv->regex_error);
544 		g_object_notify (G_OBJECT (search), "regex-error");
545 	}
546 
547 	clear_task (search);
548 
549 	search->priv->occurrences_count = 0;
550 }
551 
552 static GtkTextSearchFlags
get_text_search_flags(GtkSourceSearchContext * search)553 get_text_search_flags (GtkSourceSearchContext *search)
554 {
555 	GtkTextSearchFlags flags = GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY;
556 
557 	if (!gtk_source_search_settings_get_case_sensitive (search->priv->settings))
558 	{
559 		flags |= GTK_TEXT_SEARCH_CASE_INSENSITIVE;
560 	}
561 
562 	return flags;
563 }
564 
565 /* @start_pos is in bytes. */
566 static void
regex_search_get_real_start(GtkSourceSearchContext * search,const GtkTextIter * start,GtkTextIter * real_start,gint * start_pos)567 regex_search_get_real_start (GtkSourceSearchContext *search,
568 			     const GtkTextIter      *start,
569 			     GtkTextIter            *real_start,
570 			     gint                   *start_pos)
571 {
572 	gint max_lookbehind = g_regex_get_max_lookbehind (search->priv->regex);
573 	gint i;
574 	gchar *text;
575 
576 	*real_start = *start;
577 
578 	for (i = 0; i < max_lookbehind; i++)
579 	{
580 		if (!gtk_text_iter_backward_char (real_start))
581 		{
582 			break;
583 		}
584 	}
585 
586 	text = gtk_text_iter_get_visible_text (real_start, start);
587 	*start_pos = strlen (text);
588 
589 	g_free (text);
590 }
591 
592 static GRegexMatchFlags
regex_search_get_match_options(const GtkTextIter * real_start,const GtkTextIter * end)593 regex_search_get_match_options (const GtkTextIter *real_start,
594 				const GtkTextIter *end)
595 {
596 	GRegexMatchFlags match_options = 0;
597 
598 	if (!gtk_text_iter_starts_line (real_start))
599 	{
600 		match_options |= G_REGEX_MATCH_NOTBOL;
601 	}
602 
603 	if (!gtk_text_iter_ends_line (end))
604 	{
605 		match_options |= G_REGEX_MATCH_NOTEOL;
606 	}
607 
608 	if (!gtk_text_iter_is_end (end))
609 	{
610 		match_options |= G_REGEX_MATCH_PARTIAL_HARD;
611 	}
612 
613 	return match_options;
614 }
615 
616 /* Get the @match_start and @match_end iters of the @match_info.
617  * g_match_info_fetch_pos() returns byte positions. To get the iters, we need to
618  * know the number of UTF-8 characters. A GMatchInfo can contain several matches
619  * (with g_match_info_next()). So instead of calling g_utf8_strlen() each time
620  * at the beginning of @subject, @iter and @iter_byte_pos are used to remember
621  * where g_utf8_strlen() stopped.
622  */
623 static gboolean
regex_search_fetch_match(GMatchInfo * match_info,const gchar * subject,gssize subject_length,GtkTextIter * iter,gint * iter_byte_pos,GtkTextIter * match_start,GtkTextIter * match_end)624 regex_search_fetch_match (GMatchInfo  *match_info,
625 			  const gchar *subject,
626 			  gssize       subject_length,
627 			  GtkTextIter *iter,
628 			  gint        *iter_byte_pos,
629 			  GtkTextIter *match_start,
630 			  GtkTextIter *match_end)
631 {
632 	gint start_byte_pos;
633 	gint end_byte_pos;
634 	gint nb_chars;
635 
636 	g_assert (*iter_byte_pos <= subject_length);
637 	g_assert (match_start != NULL);
638 	g_assert (match_end != NULL);
639 
640 	if (!g_match_info_matches (match_info))
641 	{
642 		return FALSE;
643 	}
644 
645 	if (!g_match_info_fetch_pos (match_info, 0, &start_byte_pos, &end_byte_pos))
646 	{
647 		g_warning ("Impossible to fetch regex match position.");
648 		return FALSE;
649 	}
650 
651 	g_assert (start_byte_pos < subject_length);
652 	g_assert (end_byte_pos <= subject_length);
653 	g_assert (*iter_byte_pos <= start_byte_pos);
654 	g_assert (start_byte_pos < end_byte_pos);
655 
656 	nb_chars = g_utf8_strlen (subject + *iter_byte_pos,
657 				  start_byte_pos - *iter_byte_pos);
658 
659 	*match_start = *iter;
660 	gtk_text_iter_forward_chars (match_start, nb_chars);
661 
662 	nb_chars = g_utf8_strlen (subject + start_byte_pos,
663 				  end_byte_pos - start_byte_pos);
664 
665 	*match_end = *match_start;
666 	gtk_text_iter_forward_chars (match_end, nb_chars);
667 
668 	*iter = *match_end;
669 	*iter_byte_pos = end_byte_pos;
670 
671 	return TRUE;
672 }
673 
674 /* If you retrieve only [match_start, match_end] from the GtkTextBuffer, it
675  * does not match the regex if the regex contains look-ahead assertions. For
676  * that, get the @real_end. Note that [match_start, real_end] is not the minimum
677  * amount of text that still matches the regex, it can contain several
678  * occurrences, so you can add the G_REGEX_MATCH_ANCHORED option to match only
679  * the first occurrence.
680  * Note that @limit is the limit for @match_end, not @real_end.
681  */
682 static gboolean
basic_forward_regex_search(GtkSourceSearchContext * search,const GtkTextIter * start_at,GtkTextIter * match_start,GtkTextIter * match_end,GtkTextIter * real_end,const GtkTextIter * limit)683 basic_forward_regex_search (GtkSourceSearchContext *search,
684 			    const GtkTextIter      *start_at,
685 			    GtkTextIter            *match_start,
686 			    GtkTextIter            *match_end,
687 			    GtkTextIter            *real_end,
688 			    const GtkTextIter      *limit)
689 {
690 	GtkTextIter real_start;
691 	GtkTextIter end;
692 	gint start_pos;
693 	gboolean found = FALSE;
694 	gint nb_lines = 1;
695 
696 	if (search->priv->regex == NULL ||
697 	    search->priv->regex_error != NULL)
698 	{
699 		return FALSE;
700 	}
701 
702 	regex_search_get_real_start (search, start_at, &real_start, &start_pos);
703 
704 	if (limit == NULL)
705 	{
706 		gtk_text_buffer_get_end_iter (search->priv->buffer, &end);
707 	}
708 	else
709 	{
710 		end = *limit;
711 	}
712 
713 	while (TRUE)
714 	{
715 		GRegexMatchFlags match_options;
716 		gchar *subject;
717 		gssize subject_length;
718 		GMatchInfo *match_info;
719 		GtkTextIter iter;
720 		gint iter_byte_pos;
721 		GtkTextIter m_start;
722 		GtkTextIter m_end;
723 
724 		match_options = regex_search_get_match_options (&real_start, &end);
725 		subject = gtk_text_iter_get_visible_text (&real_start, &end);
726 		subject_length = strlen (subject);
727 
728 		g_regex_match_full (search->priv->regex,
729 				    subject,
730 				    subject_length,
731 				    start_pos,
732 				    match_options,
733 				    &match_info,
734 				    &search->priv->regex_error);
735 
736 		iter = real_start;
737 		iter_byte_pos = 0;
738 
739 		found = regex_search_fetch_match (match_info,
740 						  subject,
741 						  subject_length,
742 						  &iter,
743 						  &iter_byte_pos,
744 						  &m_start,
745 						  &m_end);
746 
747 		if (!found && g_match_info_is_partial_match (match_info))
748 		{
749 			gtk_text_iter_forward_lines (&end, nb_lines);
750 			nb_lines <<= 1;
751 
752 			g_free (subject);
753 			g_match_info_free (match_info);
754 			continue;
755 		}
756 
757 		/* Check that the match is not beyond the limit. This can happen
758 		 * if a partial match is found on the first iteration. Then the
759 		 * partial match was actually not a good match, but a second
760 		 * good match is found.
761 		 */
762 		if (found && limit != NULL && gtk_text_iter_compare (limit, &m_end) < 0)
763 		{
764 			found = FALSE;
765 		}
766 
767 		if (search->priv->regex_error != NULL)
768 		{
769 			g_object_notify (G_OBJECT (search), "regex-error");
770 			found = FALSE;
771 		}
772 
773 		if (found)
774 		{
775 			if (match_start != NULL)
776 			{
777 				*match_start = m_start;
778 			}
779 
780 			if (match_end != NULL)
781 			{
782 				*match_end = m_end;
783 			}
784 
785 			if (real_end != NULL)
786 			{
787 				*real_end = end;
788 			}
789 		}
790 
791 		g_free (subject);
792 		g_match_info_free (match_info);
793 		break;
794 	}
795 
796 	return found;
797 }
798 
799 static gboolean
basic_forward_search(GtkSourceSearchContext * search,const GtkTextIter * iter,GtkTextIter * match_start,GtkTextIter * match_end,const GtkTextIter * limit)800 basic_forward_search (GtkSourceSearchContext *search,
801 		      const GtkTextIter      *iter,
802 		      GtkTextIter            *match_start,
803 		      GtkTextIter            *match_end,
804 		      const GtkTextIter      *limit)
805 {
806 	GtkTextIter begin_search = *iter;
807 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
808 	GtkTextSearchFlags flags;
809 
810 	if (search_text == NULL)
811 	{
812 		return FALSE;
813 	}
814 
815 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
816 	{
817 		return basic_forward_regex_search (search,
818 						   iter,
819 						   match_start,
820 						   match_end,
821 						   NULL,
822 						   limit);
823 	}
824 
825 	flags = get_text_search_flags (search);
826 
827 	while (TRUE)
828 	{
829 		gboolean found = gtk_text_iter_forward_search (&begin_search,
830 							       search_text,
831 							       flags,
832 							       match_start,
833 							       match_end,
834 							       limit);
835 
836 		if (!found || !gtk_source_search_settings_get_at_word_boundaries (search->priv->settings))
837 		{
838 			return found;
839 		}
840 
841 		if (_gtk_source_iter_starts_extra_natural_word (match_start, FALSE) &&
842 		    _gtk_source_iter_ends_extra_natural_word (match_end, FALSE))
843 		{
844 			return TRUE;
845 		}
846 
847 		begin_search = *match_end;
848 	}
849 }
850 
851 /* We fake the backward regex search by doing a forward search, and taking the
852  * last match.
853  */
854 static gboolean
basic_backward_regex_search(GtkSourceSearchContext * search,const GtkTextIter * start_at,GtkTextIter * match_start,GtkTextIter * match_end,const GtkTextIter * limit)855 basic_backward_regex_search (GtkSourceSearchContext *search,
856 			     const GtkTextIter      *start_at,
857 			     GtkTextIter            *match_start,
858 			     GtkTextIter            *match_end,
859 			     const GtkTextIter      *limit)
860 {
861 	GtkTextIter lower_bound;
862 	GtkTextIter m_start;
863 	GtkTextIter m_end;
864 	gboolean found = FALSE;
865 
866 	if (search->priv->regex == NULL ||
867 	    search->priv->regex_error != NULL)
868 	{
869 		return FALSE;
870 	}
871 
872 	if (limit == NULL)
873 	{
874 		gtk_text_buffer_get_start_iter (search->priv->buffer, &lower_bound);
875 	}
876 	else
877 	{
878 		lower_bound = *limit;
879 	}
880 
881 	while (basic_forward_regex_search (search, &lower_bound, &m_start, &m_end, NULL, start_at))
882 	{
883 		found = TRUE;
884 
885 		if (match_start != NULL)
886 		{
887 			*match_start = m_start;
888 		}
889 
890 		if (match_end != NULL)
891 		{
892 			*match_end = m_end;
893 		}
894 
895 		lower_bound = m_end;
896 	}
897 
898 	return found;
899 }
900 
901 static gboolean
basic_backward_search(GtkSourceSearchContext * search,const GtkTextIter * iter,GtkTextIter * match_start,GtkTextIter * match_end,const GtkTextIter * limit)902 basic_backward_search (GtkSourceSearchContext *search,
903 		       const GtkTextIter      *iter,
904 		       GtkTextIter            *match_start,
905 		       GtkTextIter            *match_end,
906 		       const GtkTextIter      *limit)
907 {
908 	GtkTextIter begin_search = *iter;
909 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
910 	GtkTextSearchFlags flags;
911 
912 	if (search_text == NULL)
913 	{
914 		return FALSE;
915 	}
916 
917 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
918 	{
919 		return basic_backward_regex_search (search,
920 						    iter,
921 						    match_start,
922 						    match_end,
923 						    limit);
924 	}
925 
926 	flags = get_text_search_flags (search);
927 
928 	while (TRUE)
929 	{
930 		gboolean found = gtk_text_iter_backward_search (&begin_search,
931 							        search_text,
932 							        flags,
933 							        match_start,
934 							        match_end,
935 							        limit);
936 
937 		if (!found || !gtk_source_search_settings_get_at_word_boundaries (search->priv->settings))
938 		{
939 			return found;
940 		}
941 
942 		if (_gtk_source_iter_starts_extra_natural_word (match_start, FALSE) &&
943 		    _gtk_source_iter_ends_extra_natural_word (match_end, FALSE))
944 		{
945 			return TRUE;
946 		}
947 
948 		begin_search = *match_start;
949 	}
950 }
951 
952 static void
forward_backward_data_free(ForwardBackwardData * data)953 forward_backward_data_free (ForwardBackwardData *data)
954 {
955 	if (data->start_at != NULL)
956 	{
957 		GtkTextBuffer *buffer = gtk_text_mark_get_buffer (data->start_at);
958 		gtk_text_buffer_delete_mark (buffer, data->start_at);
959 	}
960 
961 	if (data->match_start != NULL)
962 	{
963 		GtkTextBuffer *buffer = gtk_text_mark_get_buffer (data->match_start);
964 		gtk_text_buffer_delete_mark (buffer, data->match_start);
965 	}
966 
967 	if (data->match_end != NULL)
968 	{
969 		GtkTextBuffer *buffer = gtk_text_mark_get_buffer (data->match_end);
970 		gtk_text_buffer_delete_mark (buffer, data->match_end);
971 	}
972 
973 	g_slice_free (ForwardBackwardData, data);
974 }
975 
976 /* Returns TRUE if finished. */
977 static gboolean
smart_forward_search_async_step(GtkSourceSearchContext * search,GtkTextIter * start_at,gboolean * wrapped_around)978 smart_forward_search_async_step (GtkSourceSearchContext *search,
979 				 GtkTextIter            *start_at,
980 				 gboolean               *wrapped_around)
981 {
982 	GtkTextIter iter = *start_at;
983 	GtkTextIter limit;
984 	GtkTextIter region_start = *start_at;
985 	GtkSourceRegion *region = NULL;
986 	ForwardBackwardData *task_data;
987 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
988 
989 	if (gtk_text_iter_is_end (start_at))
990 	{
991 		if (search_text != NULL &&
992 		    !*wrapped_around &&
993 		    gtk_source_search_settings_get_wrap_around (search->priv->settings))
994 		{
995 			gtk_text_buffer_get_start_iter (search->priv->buffer, start_at);
996 			*wrapped_around = TRUE;
997 			return FALSE;
998 		}
999 
1000 		task_data = g_slice_new0 (ForwardBackwardData);
1001 		task_data->found = FALSE;
1002 		task_data->is_forward = TRUE;
1003 		task_data->wrapped_around = *wrapped_around;
1004 
1005 		g_task_return_pointer (search->priv->task,
1006 				       task_data,
1007 				       (GDestroyNotify)forward_backward_data_free);
1008 
1009 		g_clear_object (&search->priv->task);
1010 		return TRUE;
1011 	}
1012 
1013 	if (!gtk_text_iter_has_tag (&iter, search->priv->found_tag))
1014 	{
1015 		gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag);
1016 	}
1017 	else if (!gtk_text_iter_starts_tag (&iter, search->priv->found_tag))
1018 	{
1019 		gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag);
1020 		region_start = iter;
1021 	}
1022 
1023 	limit = iter;
1024 	gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->found_tag);
1025 
1026 	if (search->priv->scan_region != NULL)
1027 	{
1028 		region = gtk_source_region_intersect_subregion (search->priv->scan_region,
1029 								&region_start,
1030 								&limit);
1031 	}
1032 
1033 	if (gtk_source_region_is_empty (region))
1034 	{
1035 		GtkTextIter match_start;
1036 		GtkTextIter match_end;
1037 
1038 		g_clear_object (&region);
1039 
1040 		while (basic_forward_search (search, &iter, &match_start, &match_end, &limit))
1041 		{
1042 			if (gtk_text_iter_compare (&match_start, start_at) < 0)
1043 			{
1044 				iter = match_end;
1045 				continue;
1046 			}
1047 
1048 			task_data = g_slice_new0 (ForwardBackwardData);
1049 			task_data->found = TRUE;
1050 			task_data->match_start =
1051                                 gtk_text_buffer_create_mark (search->priv->buffer,
1052                                                              NULL,
1053                                                              &match_start,
1054                                                              TRUE);
1055 			task_data->match_end =
1056                                 gtk_text_buffer_create_mark (search->priv->buffer,
1057                                                              NULL,
1058                                                              &match_end,
1059                                                              FALSE);
1060 			task_data->is_forward = TRUE;
1061 			task_data->wrapped_around = *wrapped_around;
1062 
1063 			g_task_return_pointer (search->priv->task,
1064 					       task_data,
1065 					       (GDestroyNotify)forward_backward_data_free);
1066 
1067 			g_clear_object (&search->priv->task);
1068 			return TRUE;
1069 		}
1070 
1071 		*start_at = limit;
1072 		return FALSE;
1073 	}
1074 
1075 	task_data = g_slice_new0 (ForwardBackwardData);
1076 	task_data->is_forward = TRUE;
1077 	task_data->wrapped_around = *wrapped_around;
1078 	task_data->start_at = gtk_text_buffer_create_mark (search->priv->buffer,
1079 							   NULL,
1080 							   start_at,
1081 							   TRUE);
1082 
1083 	g_task_set_task_data (search->priv->task,
1084 			      task_data,
1085 			      (GDestroyNotify)forward_backward_data_free);
1086 
1087 	g_clear_object (&search->priv->task_region);
1088 	search->priv->task_region = region;
1089 
1090 	install_idle_scan (search);
1091 
1092 	/* The idle that scan the task region will call
1093 	 * smart_forward_search_async() to continue the task. But for the
1094 	 * moment, we are done.
1095 	 */
1096 	return TRUE;
1097 }
1098 
1099 static void
smart_forward_search_async(GtkSourceSearchContext * search,const GtkTextIter * start_at,gboolean wrapped_around)1100 smart_forward_search_async (GtkSourceSearchContext *search,
1101 			    const GtkTextIter      *start_at,
1102 			    gboolean                wrapped_around)
1103 {
1104 	GtkTextIter iter = *start_at;
1105 
1106 	/* A recursive function would have been more natural, but a loop is
1107 	 * better to avoid stack overflows.
1108 	 */
1109 	while (!smart_forward_search_async_step (search, &iter, &wrapped_around));
1110 }
1111 
1112 
1113 /* Returns TRUE if finished. */
1114 static gboolean
smart_backward_search_async_step(GtkSourceSearchContext * search,GtkTextIter * start_at,gboolean * wrapped_around)1115 smart_backward_search_async_step (GtkSourceSearchContext *search,
1116 				  GtkTextIter            *start_at,
1117 				  gboolean               *wrapped_around)
1118 {
1119 	GtkTextIter iter = *start_at;
1120 	GtkTextIter limit;
1121 	GtkTextIter region_end = *start_at;
1122 	GtkSourceRegion *region = NULL;
1123 	ForwardBackwardData *task_data;
1124 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
1125 
1126 	if (gtk_text_iter_is_start (start_at))
1127 	{
1128 		if (search_text != NULL &&
1129 		    !*wrapped_around &&
1130 		    gtk_source_search_settings_get_wrap_around (search->priv->settings))
1131 		{
1132 			gtk_text_buffer_get_end_iter (search->priv->buffer, start_at);
1133 			*wrapped_around = TRUE;
1134 			return FALSE;
1135 		}
1136 
1137 		task_data = g_slice_new0 (ForwardBackwardData);
1138 		task_data->found = FALSE;
1139 		task_data->is_forward = FALSE;
1140 		task_data->wrapped_around = *wrapped_around;
1141 
1142 		g_task_return_pointer (search->priv->task,
1143 				       task_data,
1144 				       (GDestroyNotify)forward_backward_data_free);
1145 
1146 		g_clear_object (&search->priv->task);
1147 		return TRUE;
1148 	}
1149 
1150 	if (gtk_text_iter_starts_tag (&iter, search->priv->found_tag) ||
1151 	    (!gtk_text_iter_has_tag (&iter, search->priv->found_tag) &&
1152 	     !gtk_text_iter_ends_tag (&iter, search->priv->found_tag)))
1153 	{
1154 		gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag);
1155 	}
1156 	else if (gtk_text_iter_has_tag (&iter, search->priv->found_tag))
1157 	{
1158 		gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag);
1159 		region_end = iter;
1160 	}
1161 
1162 	limit = iter;
1163 	gtk_text_iter_backward_to_tag_toggle (&limit, search->priv->found_tag);
1164 
1165 	if (search->priv->scan_region != NULL)
1166 	{
1167 		region = gtk_source_region_intersect_subregion (search->priv->scan_region,
1168 								&limit,
1169 								&region_end);
1170 	}
1171 
1172 	if (gtk_source_region_is_empty (region))
1173 	{
1174 		GtkTextIter match_start;
1175 		GtkTextIter match_end;
1176 
1177 		g_clear_object (&region);
1178 
1179 		while (basic_backward_search (search, &iter, &match_start, &match_end, &limit))
1180 		{
1181 			if (gtk_text_iter_compare (start_at, &match_end) < 0)
1182 			{
1183 				iter = match_start;
1184 				continue;
1185 			}
1186 
1187 			task_data = g_slice_new0 (ForwardBackwardData);
1188 			task_data->found = TRUE;
1189 			task_data->match_start =
1190 				gtk_text_buffer_create_mark (search->priv->buffer,
1191 				                             NULL,
1192 				                             &match_start,
1193 				                             TRUE);
1194 			task_data->match_end =
1195 				gtk_text_buffer_create_mark (search->priv->buffer,
1196 				                             NULL,
1197 				                             &match_end,
1198 				                             FALSE);
1199 			task_data->is_forward = FALSE;
1200 			task_data->wrapped_around = *wrapped_around;
1201 
1202 			g_task_return_pointer (search->priv->task,
1203 					       task_data,
1204 					       (GDestroyNotify)forward_backward_data_free);
1205 
1206 			g_clear_object (&search->priv->task);
1207 			return TRUE;
1208 		}
1209 
1210 		*start_at = limit;
1211 		return FALSE;
1212 	}
1213 
1214 	task_data = g_slice_new0 (ForwardBackwardData);
1215 	task_data->is_forward = FALSE;
1216 	task_data->wrapped_around = *wrapped_around;
1217 	task_data->start_at = gtk_text_buffer_create_mark (search->priv->buffer,
1218 							   NULL,
1219 							   start_at,
1220 							   TRUE);
1221 
1222 	g_task_set_task_data (search->priv->task,
1223 			      task_data,
1224 			      (GDestroyNotify)forward_backward_data_free);
1225 
1226 	g_clear_object (&search->priv->task_region);
1227 	search->priv->task_region = region;
1228 
1229 	install_idle_scan (search);
1230 
1231 	/* The idle that scan the task region will call
1232 	 * smart_backward_search_async() to continue the task. But for the
1233 	 * moment, we are done.
1234 	 */
1235 	return TRUE;
1236 }
1237 
1238 static void
smart_backward_search_async(GtkSourceSearchContext * search,const GtkTextIter * start_at,gboolean wrapped_around)1239 smart_backward_search_async (GtkSourceSearchContext *search,
1240 			     const GtkTextIter      *start_at,
1241 			     gboolean                wrapped_around)
1242 {
1243 	GtkTextIter iter = *start_at;
1244 
1245 	/* A recursive function would have been more natural, but a loop is
1246 	 * better to avoid stack overflows.
1247 	 */
1248 	while (!smart_backward_search_async_step (search, &iter, &wrapped_around));
1249 }
1250 
1251 /* Adjust the subregion so we are sure that all matches that are visible or
1252  * partially visible between @start and @end are highlighted.
1253  */
1254 static void
adjust_subregion(GtkSourceSearchContext * search,GtkTextIter * start,GtkTextIter * end)1255 adjust_subregion (GtkSourceSearchContext *search,
1256 		  GtkTextIter            *start,
1257 		  GtkTextIter            *end)
1258 {
1259 	DEBUG ({
1260 		g_print ("adjust_subregion(), before adjusting: [%u (%u), %u (%u)]\n",
1261 			 gtk_text_iter_get_line (start), gtk_text_iter_get_offset (start),
1262 			 gtk_text_iter_get_line (end), gtk_text_iter_get_offset (end));
1263 	});
1264 
1265 	gtk_text_iter_backward_lines (start, MAX (0, search->priv->text_nb_lines - 1));
1266 	gtk_text_iter_forward_lines (end, MAX (0, search->priv->text_nb_lines - 1));
1267 
1268 	if (!gtk_text_iter_starts_line (start))
1269 	{
1270 		gtk_text_iter_set_line_offset (start, 0);
1271 	}
1272 
1273 	if (!gtk_text_iter_ends_line (end))
1274 	{
1275 		gtk_text_iter_forward_to_line_end (end);
1276 	}
1277 
1278 	/* When we are in the middle of a found_tag, a simple solution is to
1279 	 * always backward_to_tag_toggle(). The problem is that occurrences can
1280 	 * be contiguous. So a full scan of the buffer can have a O(n^2) in the
1281 	 * worst case, if we use the simple solution. Therefore we use a more
1282 	 * complicated solution, that checks if we are in an old found_tag or
1283 	 * not.
1284 	 */
1285 
1286 	if (gtk_text_iter_has_tag (start, search->priv->found_tag))
1287 	{
1288 		if (gtk_source_region_is_empty (search->priv->scan_region))
1289 		{
1290 			/* 'start' is in a correct match, we can skip it. */
1291 			gtk_text_iter_forward_to_tag_toggle (start, search->priv->found_tag);
1292 		}
1293 		else
1294 		{
1295 			GtkTextIter tag_start = *start;
1296 			GtkTextIter tag_end = *start;
1297 			GtkSourceRegion *region;
1298 
1299 			if (!gtk_text_iter_starts_tag (&tag_start, search->priv->found_tag))
1300 			{
1301 				gtk_text_iter_backward_to_tag_toggle (&tag_start, search->priv->found_tag);
1302 			}
1303 
1304 			gtk_text_iter_forward_to_tag_toggle (&tag_end, search->priv->found_tag);
1305 
1306 			region = gtk_source_region_intersect_subregion (search->priv->scan_region,
1307 									&tag_start,
1308 									&tag_end);
1309 
1310 			if (gtk_source_region_is_empty (region))
1311 			{
1312 				/* 'region' has already been scanned, so 'start' is in a
1313 				 * correct match, we can skip it.
1314 				 */
1315 				*start = tag_end;
1316 			}
1317 			else
1318 			{
1319 				/* 'region' has not already been scanned completely, so
1320 				 * 'start' is most probably in an old match that must be
1321 				 * removed.
1322 				 */
1323 				*start = tag_start;
1324 			}
1325 
1326 			g_clear_object (&region);
1327 		}
1328 	}
1329 
1330 	/* Symmetric for 'end'. */
1331 
1332 	if (gtk_text_iter_has_tag (end, search->priv->found_tag))
1333 	{
1334 		if (gtk_source_region_is_empty (search->priv->scan_region))
1335 		{
1336 			/* 'end' is in a correct match, we can skip it. */
1337 
1338 			if (!gtk_text_iter_starts_tag (end, search->priv->found_tag))
1339 			{
1340 				gtk_text_iter_backward_to_tag_toggle (end, search->priv->found_tag);
1341 			}
1342 		}
1343 		else
1344 		{
1345 			GtkTextIter tag_start = *end;
1346 			GtkTextIter tag_end = *end;
1347 			GtkSourceRegion *region;
1348 
1349 			if (!gtk_text_iter_starts_tag (&tag_start, search->priv->found_tag))
1350 			{
1351 				gtk_text_iter_backward_to_tag_toggle (&tag_start, search->priv->found_tag);
1352 			}
1353 
1354 			gtk_text_iter_forward_to_tag_toggle (&tag_end, search->priv->found_tag);
1355 
1356 			region = gtk_source_region_intersect_subregion (search->priv->scan_region,
1357 									&tag_start,
1358 									&tag_end);
1359 
1360 			if (gtk_source_region_is_empty (region))
1361 			{
1362 				/* 'region' has already been scanned, so 'end' is in a
1363 				 * correct match, we can skip it.
1364 				 */
1365 				*end = tag_start;
1366 			}
1367 			else
1368 			{
1369 				/* 'region' has not already been scanned completely, so
1370 				 * 'end' is most probably in an old match that must be
1371 				 * removed.
1372 				 */
1373 				*end = tag_end;
1374 			}
1375 
1376 			g_clear_object (&region);
1377 		}
1378 	}
1379 
1380 	DEBUG ({
1381 		g_print ("adjust_subregion(), after adjusting: [%u (%u), %u (%u)]\n",
1382 			 gtk_text_iter_get_line (start), gtk_text_iter_get_offset (start),
1383 			 gtk_text_iter_get_line (end), gtk_text_iter_get_offset (end));
1384 	});
1385 }
1386 
1387 /* Do not take into account the scan_region. Take the result with a grain of
1388  * salt. You should verify before or after calling this function that the
1389  * region has been scanned, to be sure that the returned occurrence is correct.
1390  */
1391 static gboolean
smart_forward_search_without_scanning(GtkSourceSearchContext * search,const GtkTextIter * start_at,GtkTextIter * match_start,GtkTextIter * match_end,const GtkTextIter * stop_at)1392 smart_forward_search_without_scanning (GtkSourceSearchContext *search,
1393 				       const GtkTextIter      *start_at,
1394 				       GtkTextIter            *match_start,
1395 				       GtkTextIter            *match_end,
1396 				       const GtkTextIter      *stop_at)
1397 {
1398 	GtkTextIter iter;
1399 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
1400 
1401 	g_assert (start_at != NULL);
1402 	g_assert (match_start != NULL);
1403 	g_assert (match_end != NULL);
1404 	g_assert (stop_at != NULL);
1405 
1406 	iter = *start_at;
1407 
1408 	if (search_text == NULL)
1409 	{
1410 		return FALSE;
1411 	}
1412 
1413 	while (gtk_text_iter_compare (&iter, stop_at) < 0)
1414 	{
1415 		GtkTextIter limit;
1416 
1417 		if (!gtk_text_iter_has_tag (&iter, search->priv->found_tag))
1418 		{
1419 			gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag);
1420 		}
1421 		else if (!gtk_text_iter_starts_tag (&iter, search->priv->found_tag))
1422 		{
1423 			gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag);
1424 		}
1425 
1426 		limit = iter;
1427 		gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->found_tag);
1428 
1429 		if (gtk_text_iter_compare (stop_at, &limit) < 0)
1430 		{
1431 			limit = *stop_at;
1432 		}
1433 
1434 		while (basic_forward_search (search, &iter, match_start, match_end, &limit))
1435 		{
1436 			if (gtk_text_iter_compare (start_at, match_start) <= 0)
1437 			{
1438 				return TRUE;
1439 			}
1440 
1441 			iter = *match_end;
1442 		}
1443 
1444 		iter = limit;
1445 	}
1446 
1447 	return FALSE;
1448 }
1449 
1450 /* Remove the occurrences in the range. @start and @end may be adjusted, if they
1451  * are in a found_tag region.
1452  */
1453 static void
remove_occurrences_in_range(GtkSourceSearchContext * search,GtkTextIter * start,GtkTextIter * end)1454 remove_occurrences_in_range (GtkSourceSearchContext *search,
1455 			     GtkTextIter            *start,
1456 			     GtkTextIter            *end)
1457 {
1458 	GtkTextIter iter;
1459 	GtkTextIter match_start;
1460 	GtkTextIter match_end;
1461 
1462 	if ((gtk_text_iter_has_tag (start, search->priv->found_tag) &&
1463 	     !gtk_text_iter_starts_tag (start, search->priv->found_tag)) ||
1464 	    (gtk_source_search_settings_get_at_word_boundaries (search->priv->settings) &&
1465 	     gtk_text_iter_ends_tag (start, search->priv->found_tag)))
1466 	{
1467 		gtk_text_iter_backward_to_tag_toggle (start, search->priv->found_tag);
1468 	}
1469 
1470 	if ((gtk_text_iter_has_tag (end, search->priv->found_tag) &&
1471 	     !gtk_text_iter_starts_tag (end, search->priv->found_tag)) ||
1472 	    (gtk_source_search_settings_get_at_word_boundaries (search->priv->settings) &&
1473 	     gtk_text_iter_starts_tag (end, search->priv->found_tag)))
1474 	{
1475 		gtk_text_iter_forward_to_tag_toggle (end, search->priv->found_tag);
1476 	}
1477 
1478 	iter = *start;
1479 
1480 	while (smart_forward_search_without_scanning (search, &iter, &match_start, &match_end, end))
1481 	{
1482 		if (search->priv->scan_region == NULL)
1483 		{
1484 			/* The occurrence has already been scanned, and thus
1485 			 * occurrence_count take it into account. */
1486 			search->priv->occurrences_count--;
1487 		}
1488 		else
1489 		{
1490 			GtkSourceRegion *region;
1491 
1492 			region = gtk_source_region_intersect_subregion (search->priv->scan_region,
1493 									&match_start,
1494 									&match_end);
1495 
1496 			if (gtk_source_region_is_empty (region))
1497 			{
1498 				search->priv->occurrences_count--;
1499 			}
1500 
1501 			g_clear_object (&region);
1502 		}
1503 
1504 		iter = match_end;
1505 	}
1506 
1507 	gtk_text_buffer_remove_tag (search->priv->buffer,
1508 				    search->priv->found_tag,
1509 				    start,
1510 				    end);
1511 }
1512 
1513 static void
scan_subregion(GtkSourceSearchContext * search,GtkTextIter * start,GtkTextIter * end)1514 scan_subregion (GtkSourceSearchContext *search,
1515 		GtkTextIter            *start,
1516 		GtkTextIter            *end)
1517 {
1518 	GtkTextIter iter;
1519 	GtkTextIter *limit;
1520 	gboolean found = TRUE;
1521 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
1522 
1523 	/* Make sure the 'found' tag has the priority over syntax highlighting
1524 	 * tags. */
1525 	text_tag_set_highest_priority (search->priv->found_tag,
1526 				       search->priv->buffer);
1527 
1528 	adjust_subregion (search, start, end);
1529 	remove_occurrences_in_range (search, start, end);
1530 
1531 	if (search->priv->scan_region != NULL)
1532 	{
1533 		DEBUG ({
1534 			g_print ("Region to scan, before:\n");
1535 			print_region (search->priv->scan_region);
1536 		});
1537 
1538 		gtk_source_region_subtract_subregion (search->priv->scan_region, start, end);
1539 
1540 		DEBUG ({
1541 			g_print ("Region to scan, after:\n");
1542 			print_region (search->priv->scan_region);
1543 		});
1544 	}
1545 
1546 	if (search->priv->task_region != NULL)
1547 	{
1548 		gtk_source_region_subtract_subregion (search->priv->task_region, start, end);
1549 	}
1550 
1551 	if (search_text == NULL)
1552 	{
1553 		/* We have removed the found_tag, we are done. */
1554 		return;
1555 	}
1556 
1557 	iter = *start;
1558 
1559 	if (gtk_text_iter_is_end (end))
1560 	{
1561 		limit = NULL;
1562 	}
1563 	else
1564 	{
1565 		limit = end;
1566 	}
1567 
1568 	do
1569 	{
1570 		GtkTextIter match_start;
1571 		GtkTextIter match_end;
1572 
1573 		found = basic_forward_search (search, &iter, &match_start, &match_end, limit);
1574 
1575 		if (found)
1576 		{
1577 			gtk_text_buffer_apply_tag (search->priv->buffer,
1578 						   search->priv->found_tag,
1579 						   &match_start,
1580 						   &match_end);
1581 
1582 			search->priv->occurrences_count++;
1583 		}
1584 
1585 		iter = match_end;
1586 
1587 	} while (found);
1588 }
1589 
1590 static void
scan_all_region(GtkSourceSearchContext * search,GtkSourceRegion * region)1591 scan_all_region (GtkSourceSearchContext *search,
1592 		 GtkSourceRegion        *region)
1593 {
1594 	GtkSourceRegionIter region_iter;
1595 
1596 	gtk_source_region_get_start_region_iter (region, &region_iter);
1597 
1598 	while (!gtk_source_region_iter_is_end (&region_iter))
1599 	{
1600 		GtkTextIter subregion_start;
1601 		GtkTextIter subregion_end;
1602 
1603 		if (!gtk_source_region_iter_get_subregion (&region_iter,
1604 							   &subregion_start,
1605 							   &subregion_end))
1606 		{
1607 			break;
1608 		}
1609 
1610 		scan_subregion (search,
1611 				&subregion_start,
1612 				&subregion_end);
1613 
1614 		gtk_source_region_iter_next (&region_iter);
1615 	}
1616 }
1617 
1618 /* Scan a chunk of the region. If the region is small enough, all the region
1619  * will be scanned. But if the region is big, scanning only the chunk will not
1620  * block the UI normally. Begin the scan at the beginning of the region.
1621  */
1622 static void
scan_region_forward(GtkSourceSearchContext * search,GtkSourceRegion * region)1623 scan_region_forward (GtkSourceSearchContext *search,
1624 		     GtkSourceRegion        *region)
1625 {
1626 	gint nb_remaining_lines = SCAN_BATCH_SIZE;
1627 	GtkTextIter start;
1628 	GtkTextIter end;
1629 
1630 	while (nb_remaining_lines > 0 &&
1631 	       get_first_subregion (region, &start, &end))
1632 	{
1633 		GtkTextIter limit = start;
1634 		gint start_line;
1635 		gint limit_line;
1636 
1637 		gtk_text_iter_forward_lines (&limit, nb_remaining_lines);
1638 
1639 		if (gtk_text_iter_compare (&end, &limit) < 0)
1640 		{
1641 			limit = end;
1642 		}
1643 
1644 		scan_subregion (search, &start, &limit);
1645 
1646 		gtk_source_region_subtract_subregion (region, &start, &limit);
1647 
1648 		start_line = gtk_text_iter_get_line (&start);
1649 		limit_line = gtk_text_iter_get_line (&limit);
1650 
1651 		nb_remaining_lines -= limit_line - start_line;
1652 	}
1653 }
1654 
1655 /* Same as scan_region_forward(), but begins the scan at the end of the region. */
1656 static void
scan_region_backward(GtkSourceSearchContext * search,GtkSourceRegion * region)1657 scan_region_backward (GtkSourceSearchContext *search,
1658 		      GtkSourceRegion        *region)
1659 {
1660 	gint nb_remaining_lines = SCAN_BATCH_SIZE;
1661 	GtkTextIter start;
1662 	GtkTextIter end;
1663 
1664 	while (nb_remaining_lines > 0 &&
1665 	       get_last_subregion (region, &start, &end))
1666 	{
1667 		GtkTextIter limit = end;
1668 		gint limit_line;
1669 		gint end_line;
1670 
1671 		gtk_text_iter_backward_lines (&limit, nb_remaining_lines);
1672 
1673 		if (gtk_text_iter_compare (&limit, &start) < 0)
1674 		{
1675 			limit = start;
1676 		}
1677 
1678 		scan_subregion (search, &limit, &end);
1679 
1680 		gtk_source_region_subtract_subregion (region, &limit, &end);
1681 
1682 		limit_line = gtk_text_iter_get_line (&limit);
1683 		end_line = gtk_text_iter_get_line (&end);
1684 
1685 		nb_remaining_lines -= end_line - limit_line;
1686 	}
1687 }
1688 
1689 static void
resume_task(GtkSourceSearchContext * search)1690 resume_task (GtkSourceSearchContext *search)
1691 {
1692 	ForwardBackwardData *task_data = g_task_get_task_data (search->priv->task);
1693 	GtkTextIter start_at;
1694 
1695 	g_clear_object (&search->priv->task_region);
1696 
1697 	gtk_text_buffer_get_iter_at_mark (search->priv->buffer,
1698 					  &start_at,
1699 					  task_data->start_at);
1700 
1701 	if (task_data->is_forward)
1702 	{
1703 		smart_forward_search_async (search,
1704 					    &start_at,
1705 					    task_data->wrapped_around);
1706 	}
1707 	else
1708 	{
1709 		smart_backward_search_async (search,
1710 					     &start_at,
1711 					     task_data->wrapped_around);
1712 	}
1713 }
1714 
1715 static void
scan_task_region(GtkSourceSearchContext * search)1716 scan_task_region (GtkSourceSearchContext *search)
1717 {
1718 	ForwardBackwardData *task_data = g_task_get_task_data (search->priv->task);
1719 
1720 	if (task_data->is_forward)
1721 	{
1722 		scan_region_forward (search, search->priv->task_region);
1723 	}
1724 	else
1725 	{
1726 		scan_region_backward (search, search->priv->task_region);
1727 	}
1728 
1729 	resume_task (search);
1730 }
1731 
1732 static gboolean
idle_scan_normal_search(GtkSourceSearchContext * search)1733 idle_scan_normal_search (GtkSourceSearchContext *search)
1734 {
1735 	if (search->priv->high_priority_region != NULL)
1736 	{
1737 		/* Normally the high priority region is not really big, since it
1738 		 * is the visible area on the screen. So we can highlight it in
1739 		 * one batch.
1740 		 */
1741 		scan_all_region (search, search->priv->high_priority_region);
1742 
1743 		g_clear_object (&search->priv->high_priority_region);
1744 		return G_SOURCE_CONTINUE;
1745 	}
1746 
1747 	if (search->priv->task_region != NULL)
1748 	{
1749 		scan_task_region (search);
1750 		return G_SOURCE_CONTINUE;
1751 	}
1752 
1753 	scan_region_forward (search, search->priv->scan_region);
1754 
1755 	if (gtk_source_region_is_empty (search->priv->scan_region))
1756 	{
1757 		search->priv->idle_scan_id = 0;
1758 
1759 		g_object_notify (G_OBJECT (search), "occurrences-count");
1760 
1761 		g_clear_object (&search->priv->scan_region);
1762 		return G_SOURCE_REMOVE;
1763 	}
1764 
1765 	return G_SOURCE_CONTINUE;
1766 }
1767 
1768 /* Just remove the found_tag's located in the high-priority region. For big
1769  * documents, if the pattern is modified, it can take some time to re-scan all
1770  * the buffer, so it's better to clear the highlighting as soon as possible. If
1771  * the highlighting is not cleared, the user can wrongly think that the new
1772  * pattern matches the old occurrences.
1773  * The drawback of clearing the highlighting is that for small documents, there
1774  * is some flickering.
1775  */
1776 static void
regex_search_handle_high_priority_region(GtkSourceSearchContext * search)1777 regex_search_handle_high_priority_region (GtkSourceSearchContext *search)
1778 {
1779 	GtkSourceRegion *region;
1780 	GtkSourceRegionIter region_iter;
1781 
1782 	region = gtk_source_region_intersect_region (search->priv->high_priority_region,
1783 						     search->priv->scan_region);
1784 
1785 	if (region == NULL)
1786 	{
1787 		return;
1788 	}
1789 
1790 	gtk_source_region_get_start_region_iter (region, &region_iter);
1791 
1792 	while (!gtk_source_region_iter_is_end (&region_iter))
1793 	{
1794 		GtkTextIter subregion_start;
1795 		GtkTextIter subregion_end;
1796 
1797 		if (!gtk_source_region_iter_get_subregion (&region_iter,
1798 							   &subregion_start,
1799 							   &subregion_end))
1800 		{
1801 			break;
1802 		}
1803 
1804 		gtk_text_buffer_remove_tag (search->priv->buffer,
1805 					    search->priv->found_tag,
1806 					    &subregion_start,
1807 					    &subregion_end);
1808 
1809 		gtk_source_region_iter_next (&region_iter);
1810 	}
1811 
1812 	g_clear_object (&region);
1813 }
1814 
1815 /* Returns TRUE if the segment is finished, and FALSE on partial match. */
1816 static gboolean
regex_search_scan_segment(GtkSourceSearchContext * search,const GtkTextIter * segment_start,const GtkTextIter * segment_end,GtkTextIter * stopped_at)1817 regex_search_scan_segment (GtkSourceSearchContext *search,
1818 			   const GtkTextIter      *segment_start,
1819 			   const GtkTextIter      *segment_end,
1820 			   GtkTextIter            *stopped_at)
1821 {
1822 	GtkTextIter real_start;
1823 	gint start_pos;
1824 	gchar *subject;
1825 	gssize subject_length;
1826 	GRegexMatchFlags match_options;
1827 	GMatchInfo *match_info;
1828 	GtkTextIter iter;
1829 	gint iter_byte_pos;
1830 	gboolean segment_finished;
1831 	GtkTextIter match_start;
1832 	GtkTextIter match_end;
1833 
1834 	g_assert (stopped_at != NULL);
1835 
1836 	gtk_text_buffer_remove_tag (search->priv->buffer,
1837 				    search->priv->found_tag,
1838 				    segment_start,
1839 				    segment_end);
1840 
1841 	if (search->priv->regex == NULL ||
1842 	    search->priv->regex_error != NULL)
1843 	{
1844 		*stopped_at = *segment_end;
1845 		return TRUE;
1846 	}
1847 
1848 	regex_search_get_real_start (search,
1849 				     segment_start,
1850 				     &real_start,
1851 				     &start_pos);
1852 
1853 	DEBUG ({
1854 	       g_print ("\n*** regex search - scan segment ***\n");
1855 	       g_print ("start position in the subject (in bytes): %d\n", start_pos);
1856 	});
1857 
1858 	match_options = regex_search_get_match_options (&real_start, segment_end);
1859 
1860 	if (match_options & G_REGEX_MATCH_NOTBOL)
1861 	{
1862 		DEBUG ({
1863 		       g_print ("match notbol\n");
1864 		});
1865 	}
1866 
1867 	if (match_options & G_REGEX_MATCH_NOTEOL)
1868 	{
1869 		DEBUG ({
1870 		       g_print ("match noteol\n");
1871 		});
1872 	}
1873 
1874 	if (match_options & G_REGEX_MATCH_PARTIAL_HARD)
1875 	{
1876 		DEBUG ({
1877 		       g_print ("match partial hard\n");
1878 		});
1879 	}
1880 
1881 	subject = gtk_text_iter_get_visible_text (&real_start, segment_end);
1882 	subject_length = strlen (subject);
1883 
1884 	DEBUG ({
1885 	       gchar *subject_escaped = gtk_source_utils_escape_search_text (subject);
1886 	       g_print ("subject (escaped): %s\n", subject_escaped);
1887 	       g_free (subject_escaped);
1888 	});
1889 
1890 	g_regex_match_full (search->priv->regex,
1891 			    subject,
1892 			    subject_length,
1893 			    start_pos,
1894 			    match_options,
1895 			    &match_info,
1896 			    &search->priv->regex_error);
1897 
1898 	iter = real_start;
1899 	iter_byte_pos = 0;
1900 
1901 	while (regex_search_fetch_match (match_info,
1902 					 subject,
1903 					 subject_length,
1904 					 &iter,
1905 					 &iter_byte_pos,
1906 					 &match_start,
1907 					 &match_end))
1908 	{
1909 		gtk_text_buffer_apply_tag (search->priv->buffer,
1910 					   search->priv->found_tag,
1911 					   &match_start,
1912 					   &match_end);
1913 
1914 		DEBUG ({
1915 			 gchar *match_text = gtk_text_iter_get_visible_text (&match_start, &match_end);
1916 			 gchar *match_escaped = gtk_source_utils_escape_search_text (match_text);
1917 			 g_print ("match found (escaped): %s\n", match_escaped);
1918 			 g_free (match_text);
1919 			 g_free (match_escaped);
1920 		});
1921 
1922 		search->priv->occurrences_count++;
1923 
1924 		g_match_info_next (match_info, &search->priv->regex_error);
1925 	}
1926 
1927 	if (search->priv->regex_error != NULL)
1928 	{
1929 		g_object_notify (G_OBJECT (search), "regex-error");
1930 	}
1931 
1932 	if (g_match_info_is_partial_match (match_info))
1933 	{
1934 		segment_finished = FALSE;
1935 
1936 		if (gtk_text_iter_compare (segment_start, &iter) < 0)
1937 		{
1938 			*stopped_at = iter;
1939 		}
1940 		else
1941 		{
1942 			*stopped_at = *segment_start;
1943 		}
1944 
1945 		DEBUG ({
1946 		       g_print ("partial match\n");
1947 		});
1948 	}
1949 	else
1950 	{
1951 		*stopped_at = *segment_end;
1952 		segment_finished = TRUE;
1953 	}
1954 
1955 	g_free (subject);
1956 	g_match_info_free (match_info);
1957 
1958 	return segment_finished;
1959 }
1960 
1961 static void
regex_search_scan_chunk(GtkSourceSearchContext * search,const GtkTextIter * chunk_start,const GtkTextIter * chunk_end)1962 regex_search_scan_chunk (GtkSourceSearchContext *search,
1963 			 const GtkTextIter      *chunk_start,
1964 			 const GtkTextIter      *chunk_end)
1965 {
1966 	GtkTextIter segment_start = *chunk_start;
1967 
1968 	while (gtk_text_iter_compare (&segment_start, chunk_end) < 0)
1969 	{
1970 		GtkTextIter segment_end;
1971 		GtkTextIter stopped_at;
1972 		gint nb_lines = 1;
1973 
1974 		segment_end = segment_start;
1975 		gtk_text_iter_forward_line (&segment_end);
1976 
1977 		while (!regex_search_scan_segment (search,
1978 						   &segment_start,
1979 						   &segment_end,
1980 						   &stopped_at))
1981 		{
1982 			/* TODO: performance improvement. On partial match, use
1983 			 * a GString to grow the subject.
1984 			 */
1985 
1986 			segment_start = stopped_at;
1987 			gtk_text_iter_forward_lines (&segment_end, nb_lines);
1988 			nb_lines <<= 1;
1989 		}
1990 
1991 		segment_start = stopped_at;
1992 	}
1993 
1994 	gtk_source_region_subtract_subregion (search->priv->scan_region,
1995 					      chunk_start,
1996 					      &segment_start);
1997 
1998 	if (search->priv->task_region != NULL)
1999 	{
2000 		gtk_source_region_subtract_subregion (search->priv->task_region,
2001 						      chunk_start,
2002 						      &segment_start);
2003 	}
2004 }
2005 
2006 static void
regex_search_scan_next_chunk(GtkSourceSearchContext * search)2007 regex_search_scan_next_chunk (GtkSourceSearchContext *search)
2008 {
2009 	GtkTextIter chunk_start;
2010 	GtkTextIter chunk_end;
2011 
2012 	if (gtk_source_region_is_empty (search->priv->scan_region))
2013 	{
2014 		return;
2015 	}
2016 
2017 	if (!gtk_source_region_get_bounds (search->priv->scan_region, &chunk_start, NULL))
2018 	{
2019 		return;
2020 	}
2021 
2022 	chunk_end = chunk_start;
2023 	gtk_text_iter_forward_lines (&chunk_end, SCAN_BATCH_SIZE);
2024 
2025 	regex_search_scan_chunk (search, &chunk_start, &chunk_end);
2026 }
2027 
2028 static gboolean
idle_scan_regex_search(GtkSourceSearchContext * search)2029 idle_scan_regex_search (GtkSourceSearchContext *search)
2030 {
2031 	if (search->priv->high_priority_region != NULL)
2032 	{
2033 		regex_search_handle_high_priority_region (search);
2034 		g_clear_object (&search->priv->high_priority_region);
2035 		return G_SOURCE_CONTINUE;
2036 	}
2037 
2038 	regex_search_scan_next_chunk (search);
2039 
2040 	if (search->priv->task != NULL)
2041 	{
2042 		/* Always resume the task, even if the task region has not been
2043 		 * fully scanned. The task region can be huge (the whole
2044 		 * buffer), and an occurrence can be found earlier. Obviously it
2045 		 * would be better to resume the task only if an occurrence has
2046 		 * been found in the task region. But it would be a little more
2047 		 * complicated to implement, for not a big performance
2048 		 * improvement.
2049 		 */
2050 		resume_task (search);
2051 		return G_SOURCE_CONTINUE;
2052 	}
2053 
2054 	if (gtk_source_region_is_empty (search->priv->scan_region))
2055 	{
2056 		search->priv->idle_scan_id = 0;
2057 
2058 		g_object_notify (G_OBJECT (search), "occurrences-count");
2059 
2060 		g_clear_object (&search->priv->scan_region);
2061 		return G_SOURCE_REMOVE;
2062 	}
2063 
2064 	return G_SOURCE_CONTINUE;
2065 }
2066 
2067 static gboolean
idle_scan_cb(GtkSourceSearchContext * search)2068 idle_scan_cb (GtkSourceSearchContext *search)
2069 {
2070 	if (search->priv->buffer == NULL)
2071 	{
2072 		search->priv->idle_scan_id = 0;
2073 		clear_search (search);
2074 		return G_SOURCE_REMOVE;
2075 	}
2076 
2077 	return gtk_source_search_settings_get_regex_enabled (search->priv->settings) ?
2078 	       idle_scan_regex_search (search) :
2079 	       idle_scan_normal_search (search);
2080 }
2081 
2082 static void
install_idle_scan(GtkSourceSearchContext * search)2083 install_idle_scan (GtkSourceSearchContext *search)
2084 {
2085 	if (search->priv->idle_scan_id == 0)
2086 	{
2087 		search->priv->idle_scan_id = g_idle_add ((GSourceFunc)idle_scan_cb, search);
2088 	}
2089 }
2090 
2091 /* Returns TRUE when finished. */
2092 static gboolean
smart_forward_search_step(GtkSourceSearchContext * search,GtkTextIter * start_at,GtkTextIter * match_start,GtkTextIter * match_end)2093 smart_forward_search_step (GtkSourceSearchContext *search,
2094 			   GtkTextIter            *start_at,
2095 			   GtkTextIter            *match_start,
2096 			   GtkTextIter            *match_end)
2097 {
2098 	GtkTextIter iter = *start_at;
2099 	GtkTextIter limit;
2100 	GtkTextIter region_start = *start_at;
2101 	GtkSourceRegion *region = NULL;
2102 
2103 	if (!gtk_text_iter_has_tag (&iter, search->priv->found_tag))
2104 	{
2105 		gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag);
2106 	}
2107 	else if (!gtk_text_iter_starts_tag (&iter, search->priv->found_tag))
2108 	{
2109 		gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag);
2110 		region_start = iter;
2111 	}
2112 
2113 	limit = iter;
2114 	gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->found_tag);
2115 
2116 	if (search->priv->scan_region != NULL)
2117 	{
2118 		region = gtk_source_region_intersect_subregion (search->priv->scan_region,
2119 								&region_start,
2120 								&limit);
2121 	}
2122 
2123 	if (gtk_source_region_is_empty (region))
2124 	{
2125 		g_clear_object (&region);
2126 
2127 		while (basic_forward_search (search, &iter, match_start, match_end, &limit))
2128 		{
2129 			if (gtk_text_iter_compare (start_at, match_start) <= 0)
2130 			{
2131 				return TRUE;
2132 			}
2133 
2134 			iter = *match_end;
2135 		}
2136 
2137 		*start_at = limit;
2138 		return FALSE;
2139 	}
2140 
2141 	/* Scan a chunk of the buffer, not the whole 'region'. An occurrence can
2142 	 * be found before the 'region' is scanned entirely.
2143 	 */
2144 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
2145 	{
2146 		regex_search_scan_next_chunk (search);
2147 	}
2148 	else
2149 	{
2150 		scan_region_forward (search, region);
2151 	}
2152 
2153 	g_clear_object (&region);
2154 
2155 	return FALSE;
2156 }
2157 
2158 /* Doesn't wrap around. */
2159 static gboolean
smart_forward_search(GtkSourceSearchContext * search,const GtkTextIter * start_at,GtkTextIter * match_start,GtkTextIter * match_end)2160 smart_forward_search (GtkSourceSearchContext *search,
2161 		      const GtkTextIter      *start_at,
2162 		      GtkTextIter            *match_start,
2163 		      GtkTextIter            *match_end)
2164 {
2165 	GtkTextIter iter = *start_at;
2166 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
2167 
2168 	g_return_val_if_fail (match_start != NULL, FALSE);
2169 	g_return_val_if_fail (match_end != NULL, FALSE);
2170 
2171 	if (search_text == NULL)
2172 	{
2173 		return FALSE;
2174 	}
2175 
2176 	while (!gtk_text_iter_is_end (&iter))
2177 	{
2178 		if (smart_forward_search_step (search, &iter, match_start, match_end))
2179 		{
2180 			return TRUE;
2181 		}
2182 	}
2183 
2184 	return FALSE;
2185 }
2186 
2187 /* Returns TRUE when finished. */
2188 static gboolean
smart_backward_search_step(GtkSourceSearchContext * search,GtkTextIter * start_at,GtkTextIter * match_start,GtkTextIter * match_end)2189 smart_backward_search_step (GtkSourceSearchContext *search,
2190 			    GtkTextIter            *start_at,
2191 			    GtkTextIter            *match_start,
2192 			    GtkTextIter            *match_end)
2193 {
2194 	GtkTextIter iter = *start_at;
2195 	GtkTextIter limit;
2196 	GtkTextIter region_end = *start_at;
2197 	GtkSourceRegion *region = NULL;
2198 
2199 	if (gtk_text_iter_starts_tag (&iter, search->priv->found_tag) ||
2200 	    (!gtk_text_iter_has_tag (&iter, search->priv->found_tag) &&
2201 	     !gtk_text_iter_ends_tag (&iter, search->priv->found_tag)))
2202 	{
2203 		gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag);
2204 	}
2205 	else if (gtk_text_iter_has_tag (&iter, search->priv->found_tag))
2206 	{
2207 		gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag);
2208 		region_end = iter;
2209 	}
2210 
2211 	limit = iter;
2212 	gtk_text_iter_backward_to_tag_toggle (&limit, search->priv->found_tag);
2213 
2214 	if (search->priv->scan_region != NULL)
2215 	{
2216 		region = gtk_source_region_intersect_subregion (search->priv->scan_region,
2217 								&limit,
2218 								&region_end);
2219 	}
2220 
2221 	if (gtk_source_region_is_empty (region))
2222 	{
2223 		g_clear_object (&region);
2224 
2225 		while (basic_backward_search (search, &iter, match_start, match_end, &limit))
2226 		{
2227 			if (gtk_text_iter_compare (match_end, start_at) <= 0)
2228 			{
2229 				return TRUE;
2230 			}
2231 
2232 			iter = *match_start;
2233 		}
2234 
2235 		*start_at = limit;
2236 		return FALSE;
2237 	}
2238 
2239 	/* Scan a chunk of the buffer, not the whole 'region'. An occurrence can
2240 	 * be found before the 'region' is scanned entirely.
2241 	 */
2242 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
2243 	{
2244 		regex_search_scan_next_chunk (search);
2245 	}
2246 	else
2247 	{
2248 		scan_region_forward (search, region);
2249 	}
2250 
2251 	g_clear_object (&region);
2252 
2253 	return FALSE;
2254 }
2255 
2256 /* Doesn't wrap around. */
2257 static gboolean
smart_backward_search(GtkSourceSearchContext * search,const GtkTextIter * start_at,GtkTextIter * match_start,GtkTextIter * match_end)2258 smart_backward_search (GtkSourceSearchContext *search,
2259 		       const GtkTextIter      *start_at,
2260 		       GtkTextIter            *match_start,
2261 		       GtkTextIter            *match_end)
2262 {
2263 	GtkTextIter iter = *start_at;
2264 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
2265 
2266 	g_return_val_if_fail (match_start != NULL, FALSE);
2267 	g_return_val_if_fail (match_end != NULL, FALSE);
2268 
2269 	if (search_text == NULL)
2270 	{
2271 		return FALSE;
2272 	}
2273 
2274 	while (!gtk_text_iter_is_start (&iter))
2275 	{
2276 		if (smart_backward_search_step (search, &iter, match_start, match_end))
2277 		{
2278 			return TRUE;
2279 		}
2280 	}
2281 
2282 	return FALSE;
2283 }
2284 
2285 static void
add_subregion_to_scan(GtkSourceSearchContext * search,const GtkTextIter * subregion_start,const GtkTextIter * subregion_end)2286 add_subregion_to_scan (GtkSourceSearchContext *search,
2287 		       const GtkTextIter      *subregion_start,
2288 		       const GtkTextIter      *subregion_end)
2289 {
2290 	GtkTextIter start = *subregion_start;
2291 	GtkTextIter end = *subregion_end;
2292 
2293 	if (search->priv->scan_region == NULL)
2294 	{
2295 		search->priv->scan_region = gtk_source_region_new (search->priv->buffer);
2296 	}
2297 
2298 	DEBUG ({
2299 		g_print ("add_subregion_to_scan(): region to scan, before:\n");
2300 		print_region (search->priv->scan_region);
2301 	});
2302 
2303 	gtk_source_region_add_subregion (search->priv->scan_region, &start, &end);
2304 
2305 	DEBUG ({
2306 		g_print ("add_subregion_to_scan(): region to scan, after:\n");
2307 		print_region (search->priv->scan_region);
2308 	});
2309 
2310 	install_idle_scan (search);
2311 }
2312 
2313 static void
update_regex(GtkSourceSearchContext * search)2314 update_regex (GtkSourceSearchContext *search)
2315 {
2316 	gboolean regex_error_changed = FALSE;
2317 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
2318 
2319 	if (search->priv->regex != NULL)
2320 	{
2321 		g_regex_unref (search->priv->regex);
2322 		search->priv->regex = NULL;
2323 	}
2324 
2325 	if (search->priv->regex_error != NULL)
2326 	{
2327 		g_clear_error (&search->priv->regex_error);
2328 		regex_error_changed = TRUE;
2329 	}
2330 
2331 	if (search_text != NULL &&
2332 	    gtk_source_search_settings_get_regex_enabled (search->priv->settings))
2333 	{
2334 		GRegexCompileFlags compile_flags = G_REGEX_OPTIMIZE | G_REGEX_MULTILINE;
2335 		gchar *pattern = (gchar *)search_text;
2336 
2337 		search->priv->text_nb_lines = 0;
2338 
2339 		if (!gtk_source_search_settings_get_case_sensitive (search->priv->settings))
2340 		{
2341 			compile_flags |= G_REGEX_CASELESS;
2342 		}
2343 
2344 		if (gtk_source_search_settings_get_at_word_boundaries (search->priv->settings))
2345 		{
2346 			pattern = g_strdup_printf ("\\b%s\\b", search_text);
2347 		}
2348 
2349 		search->priv->regex = g_regex_new (pattern,
2350 						   compile_flags,
2351 						   G_REGEX_MATCH_NOTEMPTY,
2352 						   &search->priv->regex_error);
2353 
2354 		if (search->priv->regex_error != NULL)
2355 		{
2356 			regex_error_changed = TRUE;
2357 		}
2358 
2359 		if (gtk_source_search_settings_get_at_word_boundaries (search->priv->settings))
2360 		{
2361 			g_free (pattern);
2362 		}
2363 	}
2364 
2365 	if (regex_error_changed)
2366 	{
2367 		g_object_notify (G_OBJECT (search), "regex-error");
2368 	}
2369 }
2370 
2371 static void
update(GtkSourceSearchContext * search)2372 update (GtkSourceSearchContext *search)
2373 {
2374 	GtkTextIter start;
2375 	GtkTextIter end;
2376 	GtkSourceBufferInternal *buffer_internal;
2377 
2378 	if (search->priv->buffer == NULL)
2379 	{
2380 		return;
2381 	}
2382 
2383 	clear_search (search);
2384 	update_regex (search);
2385 
2386 	search->priv->scan_region = gtk_source_region_new (search->priv->buffer);
2387 
2388 	gtk_text_buffer_get_bounds (search->priv->buffer, &start, &end);
2389 	add_subregion_to_scan (search, &start, &end);
2390 
2391 	/* Notify the GtkSourceViews that the search is starting, so that
2392 	 * _gtk_source_search_context_update_highlight() can be called for the
2393 	 * visible regions of the buffer.
2394 	 */
2395 	buffer_internal = _gtk_source_buffer_internal_get_from_buffer (GTK_SOURCE_BUFFER (search->priv->buffer));
2396 	_gtk_source_buffer_internal_emit_search_start (buffer_internal, search);
2397 }
2398 
2399 static void
insert_text_before_cb(GtkSourceSearchContext * search,GtkTextIter * location,gchar * text,gint length)2400 insert_text_before_cb (GtkSourceSearchContext *search,
2401 		       GtkTextIter            *location,
2402 		       gchar                  *text,
2403 		       gint                    length)
2404 {
2405 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
2406 
2407 	clear_task (search);
2408 
2409 	if (search_text != NULL &&
2410 	    !gtk_source_search_settings_get_regex_enabled (search->priv->settings))
2411 	{
2412 		GtkTextIter start = *location;
2413 		GtkTextIter end = *location;
2414 
2415 		remove_occurrences_in_range (search, &start, &end);
2416 		add_subregion_to_scan (search, &start, &end);
2417 	}
2418 }
2419 
2420 static void
insert_text_after_cb(GtkSourceSearchContext * search,GtkTextIter * location,gchar * text,gint length)2421 insert_text_after_cb (GtkSourceSearchContext *search,
2422 		      GtkTextIter            *location,
2423 		      gchar                  *text,
2424 		      gint                    length)
2425 {
2426 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
2427 	{
2428 		update (search);
2429 	}
2430 	else
2431 	{
2432 		GtkTextIter start;
2433 		GtkTextIter end;
2434 
2435 		start = end = *location;
2436 
2437 		gtk_text_iter_backward_chars (&start,
2438 					      g_utf8_strlen (text, length));
2439 
2440 		add_subregion_to_scan (search, &start, &end);
2441 	}
2442 }
2443 
2444 static void
delete_range_before_cb(GtkSourceSearchContext * search,GtkTextIter * delete_start,GtkTextIter * delete_end)2445 delete_range_before_cb (GtkSourceSearchContext *search,
2446 			GtkTextIter            *delete_start,
2447 			GtkTextIter            *delete_end)
2448 {
2449 	GtkTextIter start_buffer;
2450 	GtkTextIter end_buffer;
2451 	const gchar *search_text = gtk_source_search_settings_get_search_text (search->priv->settings);
2452 
2453 	clear_task (search);
2454 
2455 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
2456 	{
2457 		return;
2458 	}
2459 
2460 	gtk_text_buffer_get_bounds (search->priv->buffer, &start_buffer, &end_buffer);
2461 
2462 	if (gtk_text_iter_equal (delete_start, &start_buffer) &&
2463 	    gtk_text_iter_equal (delete_end, &end_buffer))
2464 	{
2465 		/* Special case when removing all the text. */
2466 		search->priv->occurrences_count = 0;
2467 		return;
2468 	}
2469 
2470 	if (search_text != NULL)
2471 	{
2472 		GtkTextIter start = *delete_start;
2473 		GtkTextIter end = *delete_end;
2474 
2475 		gtk_text_iter_backward_lines (&start, search->priv->text_nb_lines);
2476 		gtk_text_iter_forward_lines (&end, search->priv->text_nb_lines);
2477 
2478 		remove_occurrences_in_range (search, &start, &end);
2479 		add_subregion_to_scan (search, &start, &end);
2480 	}
2481 }
2482 
2483 static void
delete_range_after_cb(GtkSourceSearchContext * search,GtkTextIter * start,GtkTextIter * end)2484 delete_range_after_cb (GtkSourceSearchContext *search,
2485 		       GtkTextIter            *start,
2486 		       GtkTextIter            *end)
2487 {
2488 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
2489 	{
2490 		update (search);
2491 	}
2492 	else
2493 	{
2494 		add_subregion_to_scan (search, start, end);
2495 	}
2496 }
2497 
2498 static void
set_buffer(GtkSourceSearchContext * search,GtkSourceBuffer * buffer)2499 set_buffer (GtkSourceSearchContext *search,
2500 	    GtkSourceBuffer        *buffer)
2501 {
2502 	g_assert (search->priv->buffer == NULL);
2503 	g_assert (search->priv->tag_table == NULL);
2504 
2505 	search->priv->buffer = GTK_TEXT_BUFFER (buffer);
2506 
2507 	g_object_add_weak_pointer (G_OBJECT (buffer),
2508 				   (gpointer *)&search->priv->buffer);
2509 
2510 	search->priv->tag_table = gtk_text_buffer_get_tag_table (search->priv->buffer);
2511 	g_object_ref (search->priv->tag_table);
2512 
2513 	g_signal_connect_object (buffer,
2514 				 "insert-text",
2515 				 G_CALLBACK (insert_text_before_cb),
2516 				 search,
2517 				 G_CONNECT_SWAPPED);
2518 
2519 	g_signal_connect_object (buffer,
2520 				 "insert-text",
2521 				 G_CALLBACK (insert_text_after_cb),
2522 				 search,
2523 				 G_CONNECT_AFTER | G_CONNECT_SWAPPED);
2524 
2525 	g_signal_connect_object (buffer,
2526 				 "delete-range",
2527 				 G_CALLBACK (delete_range_before_cb),
2528 				 search,
2529 				 G_CONNECT_SWAPPED);
2530 
2531 	g_signal_connect_object (buffer,
2532 				 "delete-range",
2533 				 G_CALLBACK (delete_range_after_cb),
2534 				 search,
2535 				 G_CONNECT_AFTER | G_CONNECT_SWAPPED);
2536 
2537 	search->priv->found_tag = gtk_text_buffer_create_tag (search->priv->buffer, NULL, NULL);
2538 	g_object_ref (search->priv->found_tag);
2539 
2540 	sync_found_tag (search);
2541 
2542 	g_signal_connect_object (search->priv->buffer,
2543 				 "notify::style-scheme",
2544 				 G_CALLBACK (sync_found_tag),
2545 				 search,
2546 				 G_CONNECT_SWAPPED);
2547 
2548 	_gtk_source_buffer_add_search_context (buffer, search);
2549 }
2550 
2551 static gint
compute_number_of_lines(const gchar * text)2552 compute_number_of_lines (const gchar *text)
2553 {
2554 	const gchar *p;
2555 	gint len;
2556 	gint nb_of_lines = 1;
2557 
2558 	if (text == NULL)
2559 	{
2560 		return 0;
2561 	}
2562 
2563 	len = strlen (text);
2564 	p = text;
2565 
2566 	while (len > 0)
2567 	{
2568 		gint delimiter;
2569 		gint next_paragraph;
2570 
2571 		pango_find_paragraph_boundary (p, len, &delimiter, &next_paragraph);
2572 
2573 		if (delimiter == next_paragraph)
2574 		{
2575 			/* not found */
2576 			break;
2577 		}
2578 
2579 		p += next_paragraph;
2580 		len -= next_paragraph;
2581 		nb_of_lines++;
2582 	}
2583 
2584 	return nb_of_lines;
2585 }
2586 
2587 static void
search_text_updated(GtkSourceSearchContext * search)2588 search_text_updated (GtkSourceSearchContext *search)
2589 {
2590 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
2591 	{
2592 		search->priv->text_nb_lines = 0;
2593 	}
2594 	else
2595 	{
2596 		const gchar *text = gtk_source_search_settings_get_search_text (search->priv->settings);
2597 		search->priv->text_nb_lines = compute_number_of_lines (text);
2598 	}
2599 }
2600 
2601 static void
settings_notify_cb(GtkSourceSearchContext * search,GParamSpec * pspec,GtkSourceSearchSettings * settings)2602 settings_notify_cb (GtkSourceSearchContext  *search,
2603 		    GParamSpec              *pspec,
2604 		    GtkSourceSearchSettings *settings)
2605 {
2606 	const gchar *property = g_param_spec_get_name (pspec);
2607 
2608 	if (g_str_equal (property, "search-text"))
2609 	{
2610 		search_text_updated (search);
2611 	}
2612 
2613 	update (search);
2614 }
2615 
2616 static void
set_settings(GtkSourceSearchContext * search,GtkSourceSearchSettings * settings)2617 set_settings (GtkSourceSearchContext  *search,
2618 	      GtkSourceSearchSettings *settings)
2619 {
2620 	g_assert (search->priv->settings == NULL);
2621 
2622 	if (settings != NULL)
2623 	{
2624 		search->priv->settings = g_object_ref (settings);
2625 	}
2626 	else
2627 	{
2628 		search->priv->settings = gtk_source_search_settings_new ();
2629 	}
2630 
2631 	g_signal_connect_object (search->priv->settings,
2632 				 "notify",
2633 				 G_CALLBACK (settings_notify_cb),
2634 				 search,
2635 				 G_CONNECT_SWAPPED);
2636 
2637 	search_text_updated (search);
2638 	update (search);
2639 
2640 	g_object_notify (G_OBJECT (search), "settings");
2641 }
2642 
2643 static void
gtk_source_search_context_dispose(GObject * object)2644 gtk_source_search_context_dispose (GObject *object)
2645 {
2646 	GtkSourceSearchContext *search = GTK_SOURCE_SEARCH_CONTEXT (object);
2647 
2648 	clear_search (search);
2649 
2650 	if (search->priv->found_tag != NULL &&
2651 	    search->priv->tag_table != NULL)
2652 	{
2653 		gtk_text_tag_table_remove (search->priv->tag_table,
2654 					   search->priv->found_tag);
2655 
2656 		g_clear_object (&search->priv->found_tag);
2657 		g_clear_object (&search->priv->tag_table);
2658 	}
2659 
2660 	if (search->priv->buffer != NULL)
2661 	{
2662 		g_object_remove_weak_pointer (G_OBJECT (search->priv->buffer),
2663 					      (gpointer *)&search->priv->buffer);
2664 
2665 		search->priv->buffer = NULL;
2666 	}
2667 
2668 	g_clear_object (&search->priv->settings);
2669 
2670 	G_OBJECT_CLASS (gtk_source_search_context_parent_class)->dispose (object);
2671 }
2672 
2673 static void
gtk_source_search_context_finalize(GObject * object)2674 gtk_source_search_context_finalize (GObject *object)
2675 {
2676 	GtkSourceSearchContext *search = GTK_SOURCE_SEARCH_CONTEXT (object);
2677 
2678 	if (search->priv->regex != NULL)
2679 	{
2680 		g_regex_unref (search->priv->regex);
2681 	}
2682 
2683 	g_clear_error (&search->priv->regex_error);
2684 
2685 	G_OBJECT_CLASS (gtk_source_search_context_parent_class)->finalize (object);
2686 }
2687 
2688 static void
gtk_source_search_context_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)2689 gtk_source_search_context_get_property (GObject    *object,
2690 					guint       prop_id,
2691 					GValue     *value,
2692 					GParamSpec *pspec)
2693 {
2694 	GtkSourceSearchContext *search;
2695 
2696 	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (object));
2697 
2698 	search = GTK_SOURCE_SEARCH_CONTEXT (object);
2699 
2700 	switch (prop_id)
2701 	{
2702 		case PROP_BUFFER:
2703 			g_value_set_object (value, search->priv->buffer);
2704 			break;
2705 
2706 		case PROP_SETTINGS:
2707 			g_value_set_object (value, search->priv->settings);
2708 			break;
2709 
2710 		case PROP_HIGHLIGHT:
2711 			g_value_set_boolean (value, search->priv->highlight);
2712 			break;
2713 
2714 		case PROP_MATCH_STYLE:
2715 			g_value_set_object (value, search->priv->match_style);
2716 			break;
2717 
2718 		case PROP_OCCURRENCES_COUNT:
2719 			g_value_set_int (value, gtk_source_search_context_get_occurrences_count (search));
2720 			break;
2721 
2722 		case PROP_REGEX_ERROR:
2723 			g_value_set_pointer (value, gtk_source_search_context_get_regex_error (search));
2724 			break;
2725 
2726 		default:
2727 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
2728 			break;
2729 	}
2730 }
2731 
2732 static void
gtk_source_search_context_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)2733 gtk_source_search_context_set_property (GObject      *object,
2734 					guint         prop_id,
2735 					const GValue *value,
2736 					GParamSpec   *pspec)
2737 {
2738 	GtkSourceSearchContext *search;
2739 
2740 	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (object));
2741 
2742 	search = GTK_SOURCE_SEARCH_CONTEXT (object);
2743 
2744 	switch (prop_id)
2745 	{
2746 		case PROP_BUFFER:
2747 			set_buffer (search, g_value_get_object (value));
2748 			break;
2749 
2750 		case PROP_SETTINGS:
2751 			set_settings (search, g_value_get_object (value));
2752 			break;
2753 
2754 		case PROP_HIGHLIGHT:
2755 			gtk_source_search_context_set_highlight (search, g_value_get_boolean (value));
2756 			break;
2757 
2758 		case PROP_MATCH_STYLE:
2759 			gtk_source_search_context_set_match_style (search, g_value_get_object (value));
2760 			break;
2761 
2762 		default:
2763 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
2764 			break;
2765 	}
2766 }
2767 
2768 static void
gtk_source_search_context_class_init(GtkSourceSearchContextClass * klass)2769 gtk_source_search_context_class_init (GtkSourceSearchContextClass *klass)
2770 {
2771 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
2772 
2773 	object_class->dispose = gtk_source_search_context_dispose;
2774 	object_class->finalize = gtk_source_search_context_finalize;
2775 	object_class->get_property = gtk_source_search_context_get_property;
2776 	object_class->set_property = gtk_source_search_context_set_property;
2777 
2778 	/**
2779 	 * GtkSourceSearchContext:buffer:
2780 	 *
2781 	 * The #GtkSourceBuffer associated to the search context.
2782 	 *
2783 	 * Since: 3.10
2784 	 */
2785 	g_object_class_install_property (object_class,
2786 					 PROP_BUFFER,
2787 					 g_param_spec_object ("buffer",
2788 							      "Buffer",
2789 							      "The associated GtkSourceBuffer",
2790 							      GTK_SOURCE_TYPE_BUFFER,
2791 							      G_PARAM_READWRITE |
2792 							      G_PARAM_CONSTRUCT_ONLY |
2793 							      G_PARAM_STATIC_STRINGS));
2794 
2795 	/**
2796 	 * GtkSourceSearchContext:settings:
2797 	 *
2798 	 * The #GtkSourceSearchSettings associated to the search context.
2799 	 *
2800 	 * This property is construct-only since version 4.0.
2801 	 *
2802 	 * Since: 3.10
2803 	 */
2804 	g_object_class_install_property (object_class,
2805 					 PROP_SETTINGS,
2806 					 g_param_spec_object ("settings",
2807 							      "Settings",
2808 							      "The associated GtkSourceSearchSettings",
2809 							      GTK_SOURCE_TYPE_SEARCH_SETTINGS,
2810 							      G_PARAM_READWRITE |
2811 							      G_PARAM_CONSTRUCT_ONLY |
2812 							      G_PARAM_STATIC_STRINGS));
2813 
2814 	/**
2815 	 * GtkSourceSearchContext:highlight:
2816 	 *
2817 	 * Highlight the search occurrences.
2818 	 *
2819 	 * Since: 3.10
2820 	 */
2821 	g_object_class_install_property (object_class,
2822 					 PROP_HIGHLIGHT,
2823 					 g_param_spec_boolean ("highlight",
2824 							       "Highlight",
2825 							       "Highlight search occurrences",
2826 							       TRUE,
2827 							       G_PARAM_READWRITE |
2828 							       G_PARAM_CONSTRUCT |
2829 							       G_PARAM_STATIC_STRINGS));
2830 
2831 	/**
2832 	 * GtkSourceSearchContext:match-style:
2833 	 *
2834 	 * A #GtkSourceStyle, or %NULL for theme's scheme default style.
2835 	 *
2836 	 * Since: 3.16
2837 	 */
2838 	g_object_class_install_property (object_class,
2839 					 PROP_MATCH_STYLE,
2840 					 g_param_spec_object ("match-style",
2841 							      "Match style",
2842 							      "The text style for matches",
2843 							      GTK_SOURCE_TYPE_STYLE,
2844 							      G_PARAM_READWRITE |
2845 							      G_PARAM_CONSTRUCT |
2846 							      G_PARAM_STATIC_STRINGS));
2847 
2848 	/**
2849 	 * GtkSourceSearchContext:occurrences-count:
2850 	 *
2851 	 * The total number of search occurrences. If the search is disabled,
2852 	 * the value is 0. If the buffer is not already fully scanned, the value
2853 	 * is -1.
2854 	 *
2855 	 * Since: 3.10
2856 	 */
2857 	g_object_class_install_property (object_class,
2858 					 PROP_OCCURRENCES_COUNT,
2859 					 g_param_spec_int ("occurrences-count",
2860 							   "Occurrences count",
2861 							   "Total number of search occurrences",
2862 							   -1,
2863 							   G_MAXINT,
2864 							   0,
2865 							   G_PARAM_READABLE |
2866 							   G_PARAM_STATIC_STRINGS));
2867 
2868 	/**
2869 	 * GtkSourceSearchContext:regex-error:
2870 	 *
2871 	 * If the regex search pattern doesn't follow all the rules, this
2872 	 * #GError property will be set. If the pattern is valid, the value is
2873 	 * %NULL.
2874 	 *
2875 	 * Free with g_error_free().
2876 	 *
2877 	 * Since: 3.10
2878 	 */
2879 	g_object_class_install_property (object_class,
2880 					 PROP_REGEX_ERROR,
2881 					 g_param_spec_pointer ("regex-error",
2882 							       "Regex error",
2883 							       "Regular expression error",
2884 							       G_PARAM_READABLE |
2885 							       G_PARAM_STATIC_STRINGS));
2886 }
2887 
2888 static void
gtk_source_search_context_init(GtkSourceSearchContext * search)2889 gtk_source_search_context_init (GtkSourceSearchContext *search)
2890 {
2891 	search->priv = gtk_source_search_context_get_instance_private (search);
2892 }
2893 
2894 /**
2895  * gtk_source_search_context_new:
2896  * @buffer: a #GtkSourceBuffer.
2897  * @settings: (nullable): a #GtkSourceSearchSettings, or %NULL.
2898  *
2899  * Creates a new search context, associated with @buffer, and customized with
2900  * @settings. If @settings is %NULL, a new #GtkSourceSearchSettings object will
2901  * be created, that you can retrieve with
2902  * gtk_source_search_context_get_settings().
2903  *
2904  * Returns: a new search context.
2905  * Since: 3.10
2906  */
2907 GtkSourceSearchContext *
gtk_source_search_context_new(GtkSourceBuffer * buffer,GtkSourceSearchSettings * settings)2908 gtk_source_search_context_new (GtkSourceBuffer         *buffer,
2909 			       GtkSourceSearchSettings *settings)
2910 {
2911 	g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), NULL);
2912 	g_return_val_if_fail (settings == NULL || GTK_SOURCE_IS_SEARCH_SETTINGS (settings), NULL);
2913 
2914 	return g_object_new (GTK_SOURCE_TYPE_SEARCH_CONTEXT,
2915 			     "buffer", buffer,
2916 			     "settings", settings,
2917 			     NULL);
2918 }
2919 
2920 /**
2921  * gtk_source_search_context_get_buffer:
2922  * @search: a #GtkSourceSearchContext.
2923  *
2924  * Returns: (transfer none): the associated buffer.
2925  * Since: 3.10
2926  */
2927 GtkSourceBuffer *
gtk_source_search_context_get_buffer(GtkSourceSearchContext * search)2928 gtk_source_search_context_get_buffer (GtkSourceSearchContext *search)
2929 {
2930 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL);
2931 
2932 	return GTK_SOURCE_BUFFER (search->priv->buffer);
2933 }
2934 
2935 /**
2936  * gtk_source_search_context_get_settings:
2937  * @search: a #GtkSourceSearchContext.
2938  *
2939  * Returns: (transfer none): the search settings.
2940  * Since: 3.10
2941  */
2942 GtkSourceSearchSettings *
gtk_source_search_context_get_settings(GtkSourceSearchContext * search)2943 gtk_source_search_context_get_settings (GtkSourceSearchContext *search)
2944 {
2945 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL);
2946 
2947 	return search->priv->settings;
2948 }
2949 
2950 /**
2951  * gtk_source_search_context_get_highlight:
2952  * @search: a #GtkSourceSearchContext.
2953  *
2954  * Returns: whether to highlight the search occurrences.
2955  * Since: 3.10
2956  */
2957 gboolean
gtk_source_search_context_get_highlight(GtkSourceSearchContext * search)2958 gtk_source_search_context_get_highlight (GtkSourceSearchContext *search)
2959 {
2960 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);
2961 
2962 	return search->priv->highlight;
2963 }
2964 
2965 /**
2966  * gtk_source_search_context_set_highlight:
2967  * @search: a #GtkSourceSearchContext.
2968  * @highlight: the setting.
2969  *
2970  * Enables or disables the search occurrences highlighting.
2971  *
2972  * Since: 3.10
2973  */
2974 void
gtk_source_search_context_set_highlight(GtkSourceSearchContext * search,gboolean highlight)2975 gtk_source_search_context_set_highlight (GtkSourceSearchContext *search,
2976 					 gboolean                highlight)
2977 {
2978 	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
2979 
2980 	highlight = highlight != FALSE;
2981 
2982 	if (search->priv->highlight != highlight)
2983 	{
2984 		search->priv->highlight = highlight;
2985 		sync_found_tag (search);
2986 
2987 		g_object_notify (G_OBJECT (search), "highlight");
2988 	}
2989 }
2990 
2991 /**
2992  * gtk_source_search_context_get_match_style:
2993  * @search: a #GtkSourceSearchContext.
2994  *
2995  * Returns: (transfer none): the #GtkSourceStyle to apply on search matches.
2996  *
2997  * Since: 3.16
2998  */
2999 GtkSourceStyle *
gtk_source_search_context_get_match_style(GtkSourceSearchContext * search)3000 gtk_source_search_context_get_match_style (GtkSourceSearchContext *search)
3001 {
3002 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL);
3003 
3004 	return search->priv->match_style;
3005 }
3006 
3007 /**
3008  * gtk_source_search_context_set_match_style:
3009  * @search: a #GtkSourceSearchContext.
3010  * @match_style: (nullable): a #GtkSourceStyle, or %NULL.
3011  *
3012  * Set the style to apply on search matches. If @match_style is %NULL, default
3013  * theme's scheme 'match-style' will be used.
3014  * To enable or disable the search highlighting, use
3015  * gtk_source_search_context_set_highlight().
3016  *
3017  * Since: 3.16
3018  */
3019 void
gtk_source_search_context_set_match_style(GtkSourceSearchContext * search,GtkSourceStyle * match_style)3020 gtk_source_search_context_set_match_style (GtkSourceSearchContext *search,
3021 					   GtkSourceStyle         *match_style)
3022 {
3023 	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
3024 	g_return_if_fail (match_style == NULL || GTK_SOURCE_IS_STYLE (match_style));
3025 
3026 	if (search->priv->match_style == match_style)
3027 	{
3028 		return;
3029 	}
3030 
3031 	if (search->priv->match_style != NULL)
3032 	{
3033 		g_object_unref (search->priv->match_style);
3034 	}
3035 
3036 	search->priv->match_style = match_style;
3037 
3038 	if (match_style != NULL)
3039 	{
3040 		g_object_ref (match_style);
3041 	}
3042 
3043 	g_object_notify (G_OBJECT (search), "match-style");
3044 }
3045 
3046 /**
3047  * gtk_source_search_context_get_regex_error:
3048  * @search: a #GtkSourceSearchContext.
3049  *
3050  * Regular expression patterns must follow certain rules. If
3051  * #GtkSourceSearchSettings:search-text breaks a rule, the error can be retrieved
3052  * with this function. The error domain is #G_REGEX_ERROR.
3053  *
3054  * Free the return value with g_error_free().
3055  *
3056  * Returns: (nullable): the #GError, or %NULL if the pattern is valid.
3057  * Since: 3.10
3058  */
3059 GError *
gtk_source_search_context_get_regex_error(GtkSourceSearchContext * search)3060 gtk_source_search_context_get_regex_error (GtkSourceSearchContext *search)
3061 {
3062 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL);
3063 
3064 	if (search->priv->regex_error == NULL)
3065 	{
3066 		return NULL;
3067 	}
3068 
3069 	return g_error_copy (search->priv->regex_error);
3070 }
3071 
3072 /**
3073  * gtk_source_search_context_get_occurrences_count:
3074  * @search: a #GtkSourceSearchContext.
3075  *
3076  * Gets the total number of search occurrences. If the buffer is not already
3077  * fully scanned, the total number of occurrences is unknown, and -1 is
3078  * returned.
3079  *
3080  * Returns: the total number of search occurrences, or -1 if unknown.
3081  * Since: 3.10
3082  */
3083 gint
gtk_source_search_context_get_occurrences_count(GtkSourceSearchContext * search)3084 gtk_source_search_context_get_occurrences_count (GtkSourceSearchContext *search)
3085 {
3086 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), -1);
3087 
3088 	if (!gtk_source_region_is_empty (search->priv->scan_region))
3089 	{
3090 		return -1;
3091 	}
3092 
3093 	return search->priv->occurrences_count;
3094 }
3095 
3096 /**
3097  * gtk_source_search_context_get_occurrence_position:
3098  * @search: a #GtkSourceSearchContext.
3099  * @match_start: the start of the occurrence.
3100  * @match_end: the end of the occurrence.
3101  *
3102  * Gets the position of a search occurrence. If the buffer is not already fully
3103  * scanned, the position may be unknown, and -1 is returned. If 0 is returned,
3104  * it means that this part of the buffer has already been scanned, and that
3105  * @match_start and @match_end don't delimit an occurrence.
3106  *
3107  * Returns: the position of the search occurrence. The first occurrence has the
3108  * position 1 (not 0). Returns 0 if @match_start and @match_end don't delimit
3109  * an occurrence. Returns -1 if the position is not yet known.
3110  *
3111  * Since: 3.10
3112  */
3113 gint
gtk_source_search_context_get_occurrence_position(GtkSourceSearchContext * search,const GtkTextIter * match_start,const GtkTextIter * match_end)3114 gtk_source_search_context_get_occurrence_position (GtkSourceSearchContext *search,
3115 						   const GtkTextIter      *match_start,
3116 						   const GtkTextIter      *match_end)
3117 {
3118 	GtkTextIter m_start;
3119 	GtkTextIter m_end;
3120 	GtkTextIter iter;
3121 	gboolean found;
3122 	gint position = 0;
3123 	GtkSourceRegion *region;
3124 	gboolean empty;
3125 
3126 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), -1);
3127 	g_return_val_if_fail (match_start != NULL, -1);
3128 	g_return_val_if_fail (match_end != NULL, -1);
3129 
3130 	if (search->priv->buffer == NULL)
3131 	{
3132 		return -1;
3133 	}
3134 
3135 	/* Verify that the [match_start; match_end] region has been scanned. */
3136 
3137 	if (search->priv->scan_region != NULL)
3138 	{
3139 		region = gtk_source_region_intersect_subregion (search->priv->scan_region,
3140 								match_start,
3141 								match_end);
3142 
3143 		empty = gtk_source_region_is_empty (region);
3144 
3145 		g_clear_object (&region);
3146 
3147 		if (!empty)
3148 		{
3149 			return -1;
3150 		}
3151 	}
3152 
3153 	/* Verify that the occurrence is correct. */
3154 
3155 	found = smart_forward_search_without_scanning (search,
3156 						       match_start,
3157 						       &m_start,
3158 						       &m_end,
3159 						       match_end);
3160 
3161 	if (!found ||
3162 	    !gtk_text_iter_equal (match_start, &m_start) ||
3163 	    !gtk_text_iter_equal (match_end, &m_end))
3164 	{
3165 		return 0;
3166 	}
3167 
3168 	/* Verify that the scan region is empty between the start of the buffer
3169 	 * and the end of the occurrence.
3170 	 */
3171 
3172 	gtk_text_buffer_get_start_iter (search->priv->buffer, &iter);
3173 
3174 	if (search->priv->scan_region != NULL)
3175 	{
3176 		region = gtk_source_region_intersect_subregion (search->priv->scan_region,
3177 								&iter,
3178 								match_end);
3179 
3180 		empty = gtk_source_region_is_empty (region);
3181 
3182 		g_clear_object (&region);
3183 
3184 		if (!empty)
3185 		{
3186 			return -1;
3187 		}
3188 	}
3189 
3190 	/* Everything is fine, count the number of previous occurrences. */
3191 
3192 	while (smart_forward_search_without_scanning (search, &iter, &m_start, &m_end, match_start))
3193 	{
3194 		position++;
3195 		iter = m_end;
3196 	}
3197 
3198 	return position + 1;
3199 }
3200 
3201 /**
3202  * gtk_source_search_context_forward:
3203  * @search: a #GtkSourceSearchContext.
3204  * @iter: start of search.
3205  * @match_start: (out) (optional): return location for start of match, or %NULL.
3206  * @match_end: (out) (optional): return location for end of match, or %NULL.
3207  * @has_wrapped_around: (out) (optional): return location to know whether the
3208  *   search has wrapped around, or %NULL.
3209  *
3210  * Synchronous forward search. It is recommended to use the asynchronous
3211  * functions instead, to not block the user interface. However, if you are sure
3212  * that the @buffer is small, this function is more convenient to use.
3213  *
3214  * If the #GtkSourceSearchSettings:wrap-around property is %FALSE, this function
3215  * doesn't try to wrap around.
3216  *
3217  * The @has_wrapped_around out parameter is set independently of whether a match
3218  * is found. So if this function returns %FALSE, @has_wrapped_around will have
3219  * the same value as the #GtkSourceSearchSettings:wrap-around property.
3220  *
3221  * Returns: whether a match was found.
3222  * Since: 4.0
3223  */
3224 gboolean
gtk_source_search_context_forward(GtkSourceSearchContext * search,const GtkTextIter * iter,GtkTextIter * match_start,GtkTextIter * match_end,gboolean * has_wrapped_around)3225 gtk_source_search_context_forward (GtkSourceSearchContext *search,
3226 				   const GtkTextIter      *iter,
3227 				   GtkTextIter            *match_start,
3228 				   GtkTextIter            *match_end,
3229 				   gboolean               *has_wrapped_around)
3230 {
3231 	GtkTextIter m_start;
3232 	GtkTextIter m_end;
3233 	gboolean found;
3234 
3235 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);
3236 	g_return_val_if_fail (iter != NULL, FALSE);
3237 
3238 	if (has_wrapped_around != NULL)
3239 	{
3240 		*has_wrapped_around = FALSE;
3241 	}
3242 
3243 	if (search->priv->buffer == NULL)
3244 	{
3245 		return FALSE;
3246 	}
3247 
3248 	found = smart_forward_search (search, iter, &m_start, &m_end);
3249 
3250 	if (!found && gtk_source_search_settings_get_wrap_around (search->priv->settings))
3251 	{
3252 		GtkTextIter start_iter;
3253 		gtk_text_buffer_get_start_iter (search->priv->buffer, &start_iter);
3254 
3255 		found = smart_forward_search (search, &start_iter, &m_start, &m_end);
3256 
3257 		if (has_wrapped_around != NULL)
3258 		{
3259 			*has_wrapped_around = TRUE;
3260 		}
3261 	}
3262 
3263 	if (found && match_start != NULL)
3264 	{
3265 		*match_start = m_start;
3266 	}
3267 
3268 	if (found && match_end != NULL)
3269 	{
3270 		*match_end = m_end;
3271 	}
3272 
3273 	return found;
3274 }
3275 
3276 /**
3277  * gtk_source_search_context_forward_async:
3278  * @search: a #GtkSourceSearchContext.
3279  * @iter: start of search.
3280  * @cancellable: (nullable): a #GCancellable, or %NULL.
3281  * @callback: a #GAsyncReadyCallback to call when the operation is finished.
3282  * @user_data: the data to pass to the @callback function.
3283  *
3284  * The asynchronous version of gtk_source_search_context_forward().
3285  *
3286  * See the documentation of gtk_source_search_context_forward() for more
3287  * details.
3288  *
3289  * See the #GAsyncResult documentation to know how to use this function.
3290  *
3291  * If the operation is cancelled, the @callback will only be called if
3292  * @cancellable was not %NULL. gtk_source_search_context_forward_async() takes
3293  * ownership of @cancellable, so you can unref it after calling this function.
3294  *
3295  * Since: 3.10
3296  */
3297 void
gtk_source_search_context_forward_async(GtkSourceSearchContext * search,const GtkTextIter * iter,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)3298 gtk_source_search_context_forward_async (GtkSourceSearchContext *search,
3299 					 const GtkTextIter      *iter,
3300 					 GCancellable           *cancellable,
3301 					 GAsyncReadyCallback     callback,
3302 					 gpointer                user_data)
3303 {
3304 	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
3305 	g_return_if_fail (iter != NULL);
3306 
3307 	if (search->priv->buffer == NULL)
3308 	{
3309 		return;
3310 	}
3311 
3312 	clear_task (search);
3313 	search->priv->task = g_task_new (search, cancellable, callback, user_data);
3314 
3315 	smart_forward_search_async (search, iter, FALSE);
3316 }
3317 
3318 /**
3319  * gtk_source_search_context_forward_finish:
3320  * @search: a #GtkSourceSearchContext.
3321  * @result: a #GAsyncResult.
3322  * @match_start: (out) (optional): return location for start of match, or %NULL.
3323  * @match_end: (out) (optional): return location for end of match, or %NULL.
3324  * @has_wrapped_around: (out) (optional): return location to know whether the
3325  *   search has wrapped around, or %NULL.
3326  * @error: a #GError, or %NULL.
3327  *
3328  * Finishes a forward search started with
3329  * gtk_source_search_context_forward_async().
3330  *
3331  * See the documentation of gtk_source_search_context_forward() for more
3332  * details.
3333  *
3334  * Returns: whether a match was found.
3335  * Since: 4.0
3336  */
3337 gboolean
gtk_source_search_context_forward_finish(GtkSourceSearchContext * search,GAsyncResult * result,GtkTextIter * match_start,GtkTextIter * match_end,gboolean * has_wrapped_around,GError ** error)3338 gtk_source_search_context_forward_finish (GtkSourceSearchContext  *search,
3339 					  GAsyncResult            *result,
3340 					  GtkTextIter             *match_start,
3341 					  GtkTextIter             *match_end,
3342 					  gboolean                *has_wrapped_around,
3343 					  GError                 **error)
3344 {
3345 	ForwardBackwardData *data;
3346 	gboolean found;
3347 
3348 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);
3349 
3350 	if (has_wrapped_around != NULL)
3351 	{
3352 		*has_wrapped_around = FALSE;
3353 	}
3354 
3355 	if (search->priv->buffer == NULL)
3356 	{
3357 		return FALSE;
3358 	}
3359 
3360 	g_return_val_if_fail (g_task_is_valid (result, search), FALSE);
3361 
3362 	data = g_task_propagate_pointer (G_TASK (result), error);
3363 
3364 	if (data == NULL)
3365 	{
3366 		return FALSE;
3367 	}
3368 
3369 	found = data->found;
3370 
3371 	if (found)
3372 	{
3373 		if (match_start != NULL)
3374 		{
3375                         gtk_text_buffer_get_iter_at_mark (search->priv->buffer,
3376                                                           match_start,
3377                                                           data->match_start);
3378 		}
3379 
3380 		if (match_end != NULL)
3381 		{
3382                         gtk_text_buffer_get_iter_at_mark (search->priv->buffer,
3383                                                           match_end,
3384                                                           data->match_end);
3385 		}
3386 	}
3387 
3388 	if (has_wrapped_around != NULL)
3389 	{
3390 		*has_wrapped_around = data->wrapped_around;
3391 	}
3392 
3393 	forward_backward_data_free (data);
3394 	return found;
3395 }
3396 
3397 /**
3398  * gtk_source_search_context_backward:
3399  * @search: a #GtkSourceSearchContext.
3400  * @iter: start of search.
3401  * @match_start: (out) (optional): return location for start of match, or %NULL.
3402  * @match_end: (out) (optional): return location for end of match, or %NULL.
3403  * @has_wrapped_around: (out) (optional): return location to know whether the
3404  *   search has wrapped around, or %NULL.
3405  *
3406  * Synchronous backward search. It is recommended to use the asynchronous
3407  * functions instead, to not block the user interface. However, if you are sure
3408  * that the @buffer is small, this function is more convenient to use.
3409  *
3410  * If the #GtkSourceSearchSettings:wrap-around property is %FALSE, this function
3411  * doesn't try to wrap around.
3412  *
3413  * The @has_wrapped_around out parameter is set independently of whether a match
3414  * is found. So if this function returns %FALSE, @has_wrapped_around will have
3415  * the same value as the #GtkSourceSearchSettings:wrap-around property.
3416  *
3417  * Returns: whether a match was found.
3418  * Since: 4.0
3419  */
3420 gboolean
gtk_source_search_context_backward(GtkSourceSearchContext * search,const GtkTextIter * iter,GtkTextIter * match_start,GtkTextIter * match_end,gboolean * has_wrapped_around)3421 gtk_source_search_context_backward (GtkSourceSearchContext *search,
3422 				    const GtkTextIter      *iter,
3423 				    GtkTextIter            *match_start,
3424 				    GtkTextIter            *match_end,
3425 				    gboolean               *has_wrapped_around)
3426 {
3427 	GtkTextIter m_start;
3428 	GtkTextIter m_end;
3429 	gboolean found;
3430 
3431 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);
3432 	g_return_val_if_fail (iter != NULL, FALSE);
3433 
3434 	if (has_wrapped_around != NULL)
3435 	{
3436 		*has_wrapped_around = FALSE;
3437 	}
3438 
3439 	if (search->priv->buffer == NULL)
3440 	{
3441 		return FALSE;
3442 	}
3443 
3444 	found = smart_backward_search (search, iter, &m_start, &m_end);
3445 
3446 	if (!found && gtk_source_search_settings_get_wrap_around (search->priv->settings))
3447 	{
3448 		GtkTextIter end_iter;
3449 
3450 		gtk_text_buffer_get_end_iter (search->priv->buffer, &end_iter);
3451 
3452 		found = smart_backward_search (search, &end_iter, &m_start, &m_end);
3453 
3454 		if (has_wrapped_around != NULL)
3455 		{
3456 			*has_wrapped_around = TRUE;
3457 		}
3458 	}
3459 
3460 	if (found && match_start != NULL)
3461 	{
3462 		*match_start = m_start;
3463 	}
3464 
3465 	if (found && match_end != NULL)
3466 	{
3467 		*match_end = m_end;
3468 	}
3469 
3470 	return found;
3471 }
3472 
3473 /**
3474  * gtk_source_search_context_backward_async:
3475  * @search: a #GtkSourceSearchContext.
3476  * @iter: start of search.
3477  * @cancellable: (nullable): a #GCancellable, or %NULL.
3478  * @callback: a #GAsyncReadyCallback to call when the operation is finished.
3479  * @user_data: the data to pass to the @callback function.
3480  *
3481  * The asynchronous version of gtk_source_search_context_backward().
3482  *
3483  * See the documentation of gtk_source_search_context_backward() for more
3484  * details.
3485  *
3486  * See the #GAsyncResult documentation to know how to use this function.
3487  *
3488  * If the operation is cancelled, the @callback will only be called if
3489  * @cancellable was not %NULL. gtk_source_search_context_backward_async() takes
3490  * ownership of @cancellable, so you can unref it after calling this function.
3491  *
3492  * Since: 3.10
3493  */
3494 void
gtk_source_search_context_backward_async(GtkSourceSearchContext * search,const GtkTextIter * iter,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)3495 gtk_source_search_context_backward_async (GtkSourceSearchContext *search,
3496 					  const GtkTextIter      *iter,
3497 					  GCancellable           *cancellable,
3498 					  GAsyncReadyCallback     callback,
3499 					  gpointer                user_data)
3500 {
3501 	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
3502 	g_return_if_fail (iter != NULL);
3503 
3504 	if (search->priv->buffer == NULL)
3505 	{
3506 		return;
3507 	}
3508 
3509 	clear_task (search);
3510 	search->priv->task = g_task_new (search, cancellable, callback, user_data);
3511 
3512 	smart_backward_search_async (search, iter, FALSE);
3513 }
3514 
3515 /**
3516  * gtk_source_search_context_backward_finish:
3517  * @search: a #GtkSourceSearchContext.
3518  * @result: a #GAsyncResult.
3519  * @match_start: (out) (optional): return location for start of match, or %NULL.
3520  * @match_end: (out) (optional): return location for end of match, or %NULL.
3521  * @has_wrapped_around: (out) (optional): return location to know whether the
3522  *   search has wrapped around, or %NULL.
3523  * @error: a #GError, or %NULL.
3524  *
3525  * Finishes a backward search started with
3526  * gtk_source_search_context_backward_async().
3527  *
3528  * See the documentation of gtk_source_search_context_backward() for more
3529  * details.
3530  *
3531  * Returns: whether a match was found.
3532  * Since: 4.0
3533  */
3534 gboolean
gtk_source_search_context_backward_finish(GtkSourceSearchContext * search,GAsyncResult * result,GtkTextIter * match_start,GtkTextIter * match_end,gboolean * has_wrapped_around,GError ** error)3535 gtk_source_search_context_backward_finish (GtkSourceSearchContext  *search,
3536 					   GAsyncResult            *result,
3537 					   GtkTextIter             *match_start,
3538 					   GtkTextIter             *match_end,
3539 					   gboolean                *has_wrapped_around,
3540 					   GError                 **error)
3541 {
3542 	return gtk_source_search_context_forward_finish (search,
3543 							 result,
3544 							 match_start,
3545 							 match_end,
3546 							 has_wrapped_around,
3547 							 error);
3548 }
3549 
3550 /* If correctly replaced, returns %TRUE and @match_end is updated to point to
3551  * the replacement end.
3552  */
3553 static gboolean
regex_replace(GtkSourceSearchContext * search,const GtkTextIter * match_start,GtkTextIter * match_end,const gchar * replace,GError ** error)3554 regex_replace (GtkSourceSearchContext  *search,
3555 	       const GtkTextIter       *match_start,
3556 	       GtkTextIter             *match_end,
3557 	       const gchar             *replace,
3558 	       GError                 **error)
3559 {
3560 	GtkTextIter real_start;
3561 	GtkTextIter real_end;
3562 	GtkTextIter match_start_check;
3563 	GtkTextIter match_end_check;
3564 	GtkTextIter match_start_copy;
3565 	gint start_pos;
3566 	gchar *subject;
3567 	gchar *suffix;
3568 	gchar *subject_replaced;
3569 	GRegexMatchFlags match_options;
3570 	GError *tmp_error = NULL;
3571 	gboolean replaced = FALSE;
3572 
3573 	if (search->priv->regex == NULL ||
3574 	    search->priv->regex_error != NULL)
3575 	{
3576 		return FALSE;
3577 	}
3578 
3579 	regex_search_get_real_start (search, match_start, &real_start, &start_pos);
3580 	g_assert_cmpint (start_pos, >=, 0);
3581 
3582 	if (!basic_forward_regex_search (search,
3583 					 match_start,
3584 					 &match_start_check,
3585 					 &match_end_check,
3586 					 &real_end,
3587 					 match_end))
3588 	{
3589 		g_assert_not_reached ();
3590 	}
3591 
3592 	g_assert (gtk_text_iter_equal (match_start, &match_start_check));
3593 	g_assert (gtk_text_iter_equal (match_end, &match_end_check));
3594 
3595 	subject = gtk_text_iter_get_visible_text (&real_start, &real_end);
3596 
3597 	suffix = gtk_text_iter_get_visible_text (match_end, &real_end);
3598 	if (suffix == NULL)
3599 	{
3600 		suffix = g_strdup ("");
3601 	}
3602 
3603 	match_options = regex_search_get_match_options (&real_start, &real_end);
3604 	match_options |= G_REGEX_MATCH_ANCHORED;
3605 
3606 	subject_replaced = g_regex_replace (search->priv->regex,
3607 					    subject,
3608 					    -1,
3609 					    start_pos,
3610 					    replace,
3611 					    match_options,
3612 					    &tmp_error);
3613 
3614 	if (tmp_error != NULL)
3615 	{
3616 		g_propagate_error (error, tmp_error);
3617 		goto end;
3618 	}
3619 
3620 	g_return_val_if_fail (g_str_has_suffix (subject_replaced, suffix), FALSE);
3621 
3622 	/* Truncate subject_replaced to not contain the suffix, so we can
3623 	 * replace only [match_start, match_end], not [match_start, real_end].
3624 	 * The first solution is slightly simpler, and avoids the need to
3625 	 * re-scan [match_end, real_end] for matches, which is convenient for a
3626 	 * replace all.
3627 	 */
3628 	subject_replaced[strlen (subject_replaced) - strlen (suffix)] = '\0';
3629 	g_return_val_if_fail (strlen (subject_replaced) >= (guint)start_pos, FALSE);
3630 
3631 	match_start_copy = *match_start;
3632 
3633 	gtk_text_buffer_begin_user_action (search->priv->buffer);
3634 	gtk_text_buffer_delete (search->priv->buffer, &match_start_copy, match_end);
3635 	gtk_text_buffer_insert (search->priv->buffer, match_end, subject_replaced + start_pos, -1);
3636 	gtk_text_buffer_end_user_action (search->priv->buffer);
3637 
3638 	replaced = TRUE;
3639 
3640 end:
3641 	g_free (subject);
3642 	g_free (suffix);
3643 	g_free (subject_replaced);
3644 	return replaced;
3645 }
3646 
3647 /**
3648  * gtk_source_search_context_replace:
3649  * @search: a #GtkSourceSearchContext.
3650  * @match_start: the start of the match to replace.
3651  * @match_end: the end of the match to replace.
3652  * @replace: the replacement text.
3653  * @replace_length: the length of @replace in bytes, or -1.
3654  * @error: location to a #GError, or %NULL to ignore errors.
3655  *
3656  * Replaces a search match by another text. If @match_start and @match_end
3657  * doesn't correspond to a search match, %FALSE is returned.
3658  *
3659  * @match_start and @match_end iters are revalidated to point to the replacement
3660  * text boundaries.
3661  *
3662  * For a regular expression replacement, you can check if @replace is valid by
3663  * calling g_regex_check_replacement(). The @replace text can contain
3664  * backreferences; read the g_regex_replace() documentation for more details.
3665  *
3666  * Returns: whether the match has been replaced.
3667  * Since: 4.0
3668  */
3669 gboolean
gtk_source_search_context_replace(GtkSourceSearchContext * search,GtkTextIter * match_start,GtkTextIter * match_end,const gchar * replace,gint replace_length,GError ** error)3670 gtk_source_search_context_replace (GtkSourceSearchContext  *search,
3671 				   GtkTextIter             *match_start,
3672 				   GtkTextIter             *match_end,
3673 				   const gchar             *replace,
3674 				   gint                     replace_length,
3675 				   GError                 **error)
3676 {
3677 	GtkTextIter start;
3678 	GtkTextIter end;
3679 	GtkTextMark *start_mark;
3680 	gboolean replaced = FALSE;
3681 
3682 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);
3683 	g_return_val_if_fail (match_start != NULL, FALSE);
3684 	g_return_val_if_fail (match_end != NULL, FALSE);
3685 	g_return_val_if_fail (replace != NULL, FALSE);
3686 	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
3687 
3688 	if (search->priv->buffer == NULL)
3689 	{
3690 		return FALSE;
3691 	}
3692 
3693 	if (!smart_forward_search (search, match_start, &start, &end))
3694 	{
3695 		return FALSE;
3696 	}
3697 
3698 	if (!gtk_text_iter_equal (match_start, &start) ||
3699 	    !gtk_text_iter_equal (match_end, &end))
3700 	{
3701 		return FALSE;
3702 	}
3703 
3704 	start_mark = gtk_text_buffer_create_mark (search->priv->buffer, NULL, &start, TRUE);
3705 
3706 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
3707 	{
3708 		replaced = regex_replace (search, &start, &end, replace, error);
3709 	}
3710 	else
3711 	{
3712 		gtk_text_buffer_begin_user_action (search->priv->buffer);
3713 		gtk_text_buffer_delete (search->priv->buffer, &start, &end);
3714 		gtk_text_buffer_insert (search->priv->buffer, &end, replace, replace_length);
3715 		gtk_text_buffer_end_user_action (search->priv->buffer);
3716 
3717 		replaced = TRUE;
3718 	}
3719 
3720 	if (replaced)
3721 	{
3722 		gtk_text_buffer_get_iter_at_mark (search->priv->buffer, match_start, start_mark);
3723 		*match_end = end;
3724 	}
3725 
3726 	gtk_text_buffer_delete_mark (search->priv->buffer, start_mark);
3727 
3728 	return replaced;
3729 }
3730 
3731 /**
3732  * gtk_source_search_context_replace_all:
3733  * @search: a #GtkSourceSearchContext.
3734  * @replace: the replacement text.
3735  * @replace_length: the length of @replace in bytes, or -1.
3736  * @error: location to a #GError, or %NULL to ignore errors.
3737  *
3738  * Replaces all search matches by another text. It is a synchronous function, so
3739  * it can block the user interface.
3740  *
3741  * For a regular expression replacement, you can check if @replace is valid by
3742  * calling g_regex_check_replacement(). The @replace text can contain
3743  * backreferences; read the g_regex_replace() documentation for more details.
3744  *
3745  * Returns: the number of replaced matches.
3746  * Since: 3.10
3747  */
3748 guint
gtk_source_search_context_replace_all(GtkSourceSearchContext * search,const gchar * replace,gint replace_length,GError ** error)3749 gtk_source_search_context_replace_all (GtkSourceSearchContext  *search,
3750 				       const gchar             *replace,
3751 				       gint                     replace_length,
3752 				       GError                 **error)
3753 {
3754 	GtkTextIter iter;
3755 	GtkTextIter match_start;
3756 	GtkTextIter match_end;
3757 	guint nb_matches_replaced = 0;
3758 	gboolean highlight_matching_brackets;
3759 	gboolean has_regex_references = FALSE;
3760 
3761 	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), 0);
3762 	g_return_val_if_fail (replace != NULL, 0);
3763 	g_return_val_if_fail (error == NULL || *error == NULL, 0);
3764 
3765 	if (search->priv->buffer == NULL)
3766 	{
3767 		return 0;
3768 	}
3769 
3770 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
3771 	{
3772 		GError *tmp_error = NULL;
3773 
3774 		if (search->priv->regex == NULL ||
3775 		    search->priv->regex_error != NULL)
3776 		{
3777 			return 0;
3778 		}
3779 
3780 		g_regex_check_replacement (replace,
3781 					   &has_regex_references,
3782 					   &tmp_error);
3783 
3784 		if (tmp_error != NULL)
3785 		{
3786 			g_propagate_error (error, tmp_error);
3787 			return 0;
3788 		}
3789 	}
3790 
3791 	g_signal_handlers_block_by_func (search->priv->buffer, insert_text_before_cb, search);
3792 	g_signal_handlers_block_by_func (search->priv->buffer, insert_text_after_cb, search);
3793 	g_signal_handlers_block_by_func (search->priv->buffer, delete_range_before_cb, search);
3794 	g_signal_handlers_block_by_func (search->priv->buffer, delete_range_after_cb, search);
3795 
3796 	highlight_matching_brackets =
3797 		gtk_source_buffer_get_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->priv->buffer));
3798 
3799 	gtk_source_buffer_set_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->priv->buffer),
3800 							   FALSE);
3801 
3802 	_gtk_source_buffer_save_and_clear_selection (GTK_SOURCE_BUFFER (search->priv->buffer));
3803 
3804 	gtk_text_buffer_get_start_iter (search->priv->buffer, &iter);
3805 
3806 	gtk_text_buffer_begin_user_action (search->priv->buffer);
3807 
3808 	while (smart_forward_search (search, &iter, &match_start, &match_end))
3809 	{
3810 		if (has_regex_references)
3811 		{
3812 			if (!regex_replace (search, &match_start, &match_end, replace, error))
3813 			{
3814 				break;
3815 			}
3816 		}
3817 		else
3818 		{
3819 			gtk_text_buffer_delete (search->priv->buffer, &match_start, &match_end);
3820 			gtk_text_buffer_insert (search->priv->buffer, &match_end, replace, replace_length);
3821 		}
3822 
3823 		nb_matches_replaced++;
3824 		iter = match_end;
3825 	}
3826 
3827 	gtk_text_buffer_end_user_action (search->priv->buffer);
3828 
3829 	_gtk_source_buffer_restore_selection (GTK_SOURCE_BUFFER (search->priv->buffer));
3830 
3831 	gtk_source_buffer_set_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->priv->buffer),
3832 							   highlight_matching_brackets);
3833 
3834 	g_signal_handlers_unblock_by_func (search->priv->buffer, insert_text_before_cb, search);
3835 	g_signal_handlers_unblock_by_func (search->priv->buffer, insert_text_after_cb, search);
3836 	g_signal_handlers_unblock_by_func (search->priv->buffer, delete_range_before_cb, search);
3837 	g_signal_handlers_unblock_by_func (search->priv->buffer, delete_range_after_cb, search);
3838 
3839 	update (search);
3840 
3841 	return nb_matches_replaced;
3842 }
3843 
3844 /* Highlight the [start,end] region in priority. */
3845 void
_gtk_source_search_context_update_highlight(GtkSourceSearchContext * search,const GtkTextIter * start,const GtkTextIter * end,gboolean synchronous)3846 _gtk_source_search_context_update_highlight (GtkSourceSearchContext *search,
3847 					     const GtkTextIter      *start,
3848 					     const GtkTextIter      *end,
3849 					     gboolean                synchronous)
3850 {
3851 	GtkSourceRegion *region_to_highlight = NULL;
3852 
3853 	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
3854 	g_return_if_fail (start != NULL);
3855 	g_return_if_fail (end != NULL);
3856 
3857 	if (search->priv->buffer == NULL ||
3858 	    gtk_source_region_is_empty (search->priv->scan_region) ||
3859 	    !search->priv->highlight)
3860 	{
3861 		return;
3862 	}
3863 
3864 	region_to_highlight = gtk_source_region_intersect_subregion (search->priv->scan_region,
3865 								     start,
3866 								     end);
3867 
3868 	if (gtk_source_region_is_empty (region_to_highlight))
3869 	{
3870 		goto out;
3871 	}
3872 
3873 	if (!synchronous)
3874 	{
3875 		if (search->priv->high_priority_region == NULL)
3876 		{
3877 			search->priv->high_priority_region = region_to_highlight;
3878 			region_to_highlight = NULL;
3879 		}
3880 		else
3881 		{
3882 			gtk_source_region_add_region (search->priv->high_priority_region,
3883 						      region_to_highlight);
3884 		}
3885 
3886 		install_idle_scan (search);
3887 		goto out;
3888 	}
3889 
3890 	if (gtk_source_search_settings_get_regex_enabled (search->priv->settings))
3891 	{
3892 		GtkTextIter region_start;
3893 
3894 		if (!gtk_source_region_get_bounds (search->priv->scan_region,
3895 						   &region_start,
3896 						   NULL))
3897 		{
3898 			goto out;
3899 		}
3900 
3901 		regex_search_scan_chunk (search, &region_start, end);
3902 	}
3903 	else
3904 	{
3905 		scan_all_region (search, region_to_highlight);
3906 	}
3907 
3908 out:
3909 	g_clear_object (&region_to_highlight);
3910 }
3911