/*
 * This file is part of GtkSourceView
 *
 * Copyright 2013-2016 - Sébastien Wilmet <swilmet@gnome.org>
 *
 * GtkSourceView is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * GtkSourceView is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

#include "gtksourcesearchcontext-private.h"
#include "gtksourcesearchsettings.h"
#include "gtksourcebuffer.h"
#include "gtksourcebuffer-private.h"
#include "gtksourcebufferinternal-private.h"
#include "gtksourcestyle.h"
#include "gtksourcestylescheme.h"
#include "gtksourceutils.h"
#include "gtksourceregion.h"
#include "gtksourceiter-private.h"
#include "gtksource-enumtypes.h"

#include "implregex-private.h"

/**
 * GtkSourceSearchContext:
 *
 * Search context.
 *
 * A `GtkSourceSearchContext` is used for the search and replace in a
 * [class@Buffer]. The search settings are represented by a
 * [class@SearchSettings] object. There can be a many-to-many relationship
 * between buffers and search settings, with the search contexts in-between: a
 * search settings object can be shared between several search contexts; and a
 * buffer can contain several search contexts at the same time.
 *
 * The total number of search occurrences can be retrieved with
 * [method@SearchContext.get_occurrences_count]. To know the position of a
 * certain match, use [method@SearchContext.get_occurrence_position].
 *
 * The buffer is scanned asynchronously, so it doesn't block the user interface.
 * For each search, the buffer is scanned at most once. After that, navigating
 * through the occurrences doesn't require to re-scan the buffer entirely.
 *
 * To search forward, use [method@SearchContext.forward] or
 * [method@SearchContext.forward_async] for the asynchronous version.
 * The backward search is done similarly. To replace a search match, or all
 * matches, use [method@SearchContext.replace] and
 * [method@SearchContext.replace_all].
 *
 * The search occurrences are highlighted by default. To disable it, use
 * [method@SearchContext.set_highlight]. You can enable the search
 * highlighting for several `GtkSourceSearchContext`s attached to the
 * same buffer. Moreover, each of those `GtkSourceSearchContext`s can
 * have a different text style associated. Use
 * [method@SearchContext.set_match_style] to specify the [class@Style]
 * to apply on search matches.
 *
 * Note that the [property@SearchContext:highlight] and
 * [property@SearchContext:match-style] properties are in the
 * `GtkSourceSearchContext` class, not [class@SearchSettings]. Appearance
 * settings should be tied to one, and only one buffer, as different buffers can
 * have different style scheme associated (a [class@SearchSettings] object
 * can be bound indirectly to several buffers).
 *
 * The concept of "current match" doesn't exist yet. A way to highlight
 * differently the current match is to select it.
 *
 * A search occurrence's position doesn't depend on the cursor position or other
 * parameters. Take for instance the buffer "aaaa" with the search text "aa".
 * The two occurrences are at positions [0:2] and [2:4]. If you begin to search
 * at position 1, you will get the occurrence [2:4], not [1:3]. This is a
 * prerequisite for regular expression searches. The pattern ".*" matches the
 * entire line. If the cursor is at the middle of the line, you don't want the
 * rest of the line as the occurrence, you want an entire line. (As a side note,
 * regular expression searches can also match multiple lines.)
 *
 * In the GtkSourceView source code, there is an example of how to use the
 * search and replace API: see the tests/test-search.c file. It is a mini
 * application for the search and replace, with a basic user interface.
 */

/* Implementation overview:
 *
 * When the state of the search changes (the text to search or the options), we
 * have to update the highlighting and the properties values (the number of
 * occurrences). To do so, a simple solution is to first remove all the
 * found_tag, so we have a clean buffer to analyze. The problem with this
 * solution is that there is some flickering when the user modifies the text to
 * search, because removing all the found_tag's can take some time. So we keep
 * the old found_tag's, and when we must highlight the matches in a certain
 * region, we first remove the found_tag's in this region and then we highlight
 * the newly found matches by applying the found_tag to them.
 *
 * If we only want to highlight the matches, without counting the number of
 * occurrences, a good solution would be to highlight only the visible region of
 * the buffer on the screen. So it would be useless to always scan all the
 * buffer.
 *
 * But we actually want the number of occurrences! So we have to scan all the
 * buffer. When the state of the search changes, an idle callback is installed,
 * which will scan the buffer to highlight the matches. To avoid flickering, the
 * visible region on the screen is put in a higher priority region to highlight,
 * so the idle callback will first scan this region.
 *
 * Why highlighting the non-visible matches? What we want is to (1) highlight
 * the visible matches and (2) count the number of occurrences. The code would
 * indeed be simpler if these two tasks were clearly separated (in two different
 * idle callbacks, with different regions to scan). With this simpler solution,
 * we would always use forward_search() and backward_search() to navigate
 * through the occurrences. But we can do better than that!
 * forward_to_tag_toggle() and backward_to_tag_toggle() are far more efficient:
 * once the buffer has been scanned, going to the previous or the next
 * occurrence is done in O(log n), with n the length of the buffer. We must just
 * pay attention to contiguous matches.
 *
 * While the user is typing the text in the search entry, the buffer is scanned
 * to count the number of occurrences. And when the user wants to do an
 * operation (go to the next occurrence for example), chances are that the
 * buffer has already been scanned entirely, so almost all the operations will
 * be really fast.
 *
 * Extreme example:
 * <occurrence> [1 GB of text] <next-occurrence>
 * Once the buffer is scanned, switching between the occurrences will be almost
 * instantaneous.
 *
 * So how to count the number of occurrences then? Remember that the buffer
 * contents can be modified during the scan, and that we keep the old
 * found_tag's. Moreover, when we encounter an old found_tag region, in the
 * general case we can not say how many occurrences there are in this region,
 * since a found_tag region can contain contiguous matches. Take for example the
 * found_tag region "aa": was it the "aa" search match, or two times "a"?
 * The implemented solution is to set occurrences_count to 0 when the search
 * state changes, even if old matches are still there. Because it is not
 * possible to count the old matches to decrement occurrences_count (and storing
 * the previous search text would not be sufficient, because even older matches
 * can still be there). To increment and decrement occurrences_count, there is
 * the scan_region, the region to scan. If an occurrence is contained in
 * scan_region, it means that it has not already been scanned, so
 * occurrences_count doesn't take into account this occurrence. On the other
 * hand, if we find an occurrence outside scan_region, the occurrence is
 * normally correctly highlighted, and occurrences_count take it into account.
 *
 * So when we highlight or when we remove the highlight of an occurrence (on
 * text insertion, deletion, when scanning, etc.), we increment or decrement
 * occurrences_count depending on whether the occurrence was already taken into
 * account by occurrences_count.
 *
 * If the code seems too complicated and contains strange bugs, you have two
 * choices:
 * - Write more unit tests, understand correctly the code and fix it.
 * - Rewrite the code to implement the simpler solution explained above :-)
 *   But with the simpler solution, multiple-lines regex search matches (see
 *   below) would not be possible for going to the previous occurrence (or the
 *   buffer must always be scanned from the beginning).
 *
 * Known issue
 * -----------
 *
 * Contiguous matches have a performance problem. In some cases it can lead to
 * an O(N^2) time complexity. For example if the buffer is full of contiguous
 * matches, and we want to navigate through all of them. If an iter is in the
 * middle of a found_tag region, we don't know where are the nearest occurrence
 * boundaries. Take for example the buffer "aaaa" with the search text "aa". The
 * two occurrences are at positions [0:2] and [2:4]. If we begin to search at
 * position 1, we can not take [1:3] as an occurrence. So what the code do is to
 * go backward to the start of the found_tag region, and do a basic search
 * inside the found_tag region to find the occurrence boundaries.
 *
 * So this is really a corner case, but it's better to be aware of that.
 * To fix the problem, one solution would be to have two found_tag, and
 * alternate them for contiguous matches.
 */

/* Regex search:
 *
 * With a regex, we don't know how many lines a match can span. A regex will
 * most probably match only one line, but a regex can contain something like
 * "\n*", or the dot metacharacter can also match newlines, with the "?s" option
 * (see G_REGEX_DOTALL).
 * Therefore a simple solution is to always begin the search at the beginning of
 * the document. Only the scan_region is taken into account for scanning the
 * buffer.
 *
 * For non-regex searches, when there is an insertion or deletion in the buffer,
 * we don't need to re-scan all the buffer. If there is an unmodified match in
 * the neighborhood, no need to re-scan it (unless at_word_boundaries is set).
 * For a regex search, it is more complicated. An insertion or deletion outside
 * a match can modify a match located in the neighborhood. Take for example the
 * regex "(aa)+" with the buffer contents "aaa". There is one occurrence: the
 * first two letters. If we insert an extra 'a' at the end of the buffer, the
 * occurrence is modified to take the next two letters. That's why the buffer
 * is re-scanned entirely on each insertion or deletion in the buffer.
 *
 * For searching the matches, the easiest solution is to retrieve all the buffer
 * contents, and search the occurrences on this big string. But it takes a lot
 * of memory space. It is better to do multi-segment matching, also called
 * incremental matching. See the pcrepartial(3) manpage. The matching is done
 * segment by segment, with the G_REGEX_MATCH_PARTIAL_HARD flag (for reasons
 * explained in the manpage). We begin by the first segment of the buffer as the
 * subject string. If a partial match is returned, we append the next segment to
 * the subject string, and we try again to find a complete match. When a
 * complete match is returned, we must continue to search the next occurrences.
 * The max lookbehind of the pattern must be retrieved. The start of the next
 * subject string is located at max_lookbehind characters before the end of the
 * previously found match. Similarly, if no match is found (neither a complete
 * match nor a partial match), we take the next segment, with the last
 * max_lookbehind characters from the previous segment.
 *
 * Improvement idea
 * ----------------
 *
 * What we would like to support in applications is the incremental search:
 * while we type the pattern, the buffer is scanned and the matches are
 * highlighted. When the pattern is not fully typed, strange things can happen,
 * including a pattern that match the entire buffer. And if the user is
 * working on a really big file, catastrophe: the UI is blocked!
 * To avoid this problem, a solution is to search the buffer differently
 * depending on the situation:
 * - First situation: the subject string to scan is small enough, we retrieve it
 *   and scan it directly.
 * - Second situation: the subject string to scan is too big, it will take
 *   too much time to retrieve it and scan it directly. We handle this situation
 *   in three phases: (1) retrieving the subject string, chunks by chunks, in
 *   several idle loop iterations. (2) Once the subject string is retrieved
 *   completely, we launch the regex matching in a thread. (3) Once the thread
 *   is finished, we highlight the matches in the buffer. And voilà.
 *
 * But in practice, when trying a pattern that match the entire buffer, we
 * quickly get an error like:
 *
 * 	Regex matching error: Error while matching regular expression (.*\n)*:
 * 	recursion limit reached
 *
 * It happens with test-search, with this file as the buffer (~3500 lines).
 *
 * Improvement idea
 * ----------------
 *
 * GRegex currently doesn't support JIT pattern compilation:
 * https://bugzilla.gnome.org/show_bug.cgi?id=679155
 *
 * Once available, it can be a nice performance improvement (but it must be
 * measured, since g_regex_new() is slower with JIT enabled).
 *
 * Known issue
 * -----------
 *
 * To search at word boundaries, \b is added at the beginning and at the
 * end of the pattern. But \b is not the same as
 * _gtk_source_iter_starts_extra_natural_word() and
 * _gtk_source_iter_ends_extra_natural_word(). \b for
 * example doesn't take the underscore as a word boundary.
 * Using _gtk_source_iter_starts_extra_natural_word() and ends_word() for regex searches
 * is not easily possible: if the GRegex returns a match, but doesn't
 * start and end a word, maybe a shorter match (for a greedy pattern)
 * start and end a word, or a longer match (for an ungreedy pattern). To
 * be able to use the _gtk_source_iter_starts_extra_natural_word() and ends_word()
 * functions for regex search, g_regex_match_all_full() must be used, to
 * retrieve _all_ matches, and test the word boundaries until a match is
 * found at word boundaries.
 */

/*
#define ENABLE_DEBUG
*/
#undef ENABLE_DEBUG

#ifdef ENABLE_DEBUG
#define DEBUG(x) (x)
#else
#define DEBUG(x)
#endif

/* Maximum number of lines to scan in one batch.
 * A lower value means more overhead when scanning the buffer asynchronously.
 */
#define SCAN_BATCH_SIZE 100

enum
{
	PROP_0,
	PROP_BUFFER,
	PROP_SETTINGS,
	PROP_HIGHLIGHT,
	PROP_MATCH_STYLE,
	PROP_OCCURRENCES_COUNT,
	PROP_REGEX_ERROR,
	N_PROPS
};

struct _GtkSourceSearchContext
{
	GObject parent_instance;

	/* Weak ref to the buffer. The buffer has also a weak ref to the search
	 * context. A strong ref in either direction would prevent the pointed
	 * object to be finalized.
	 */
	GtkTextBuffer *buffer;

	GtkSourceSearchSettings *settings;

	/* The tag to apply to search occurrences. Even if the highlighting is
	 * disabled, the tag is applied.
	 */
	GtkTextTag *found_tag;

	/* A reference to the tag table where the found_tag is added. The sole
	 * purpose is to remove the found_tag in dispose(). We can not rely on
	 * 'buffer' since it is a weak reference.
	 */
	GtkTextTagTable *tag_table;

	/* The region to scan and highlight. If NULL, the scan is finished. */
	GtkSourceRegion *scan_region;

	/* The region to scan and highlight in priority. I.e. the visible part
	 * of the buffer on the screen.
	 */
	GtkSourceRegion *high_priority_region;

	/* An asynchronous running task. task_region has a higher priority than
	 * scan_region, but a lower priority than high_priority_region.
	 */
	GTask *task;
	GtkSourceRegion *task_region;

	/* If the regex search is disabled, text_nb_lines is the number of lines
	 * of the search text. It is useful to adjust the region to scan.
	 */
	gint text_nb_lines;

	ImplRegex *regex;
	GError *regex_error;

	gint occurrences_count;
	gulong idle_scan_id;

	GtkSourceStyle *match_style;
	guint highlight : 1;
};

/* Data for the asynchronous forward and backward search tasks. */
typedef struct
{
	GtkTextMark *start_at;
	GtkTextMark *match_start;
	GtkTextMark *match_end;
	guint found : 1;
	guint wrapped_around : 1;

	/* forward or backward */
	guint is_forward : 1;
} ForwardBackwardData;

G_DEFINE_TYPE (GtkSourceSearchContext, gtk_source_search_context, G_TYPE_OBJECT)

static GParamSpec *properties[N_PROPS];

static void install_idle_scan (GtkSourceSearchContext *search);

#ifdef ENABLE_DEBUG
static void
print_region (GtkSourceRegion *region)
{
	gchar *str;

	str = gtk_source_region_to_string (region);
	g_print ("%s\n", str);
	g_free (str);
}
#endif

static void
sync_found_tag (GtkSourceSearchContext *search)
{
	GtkSourceStyle *style = search->match_style;
	GtkSourceStyleScheme *style_scheme;

	if (search->buffer == NULL)
	{
		return;
	}

	if (!search->highlight)
	{
		gtk_source_style_apply (NULL, search->found_tag);
		return;
	}

	if (style == NULL)
	{
		style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (search->buffer));

		if (style_scheme != NULL)
		{
			style = gtk_source_style_scheme_get_style (style_scheme, "search-match");
		}
	}

	if (style == NULL)
	{
		g_warning ("No match style defined nor 'search-match' style available.");
	}

	gtk_source_style_apply (style, search->found_tag);
}

static void
text_tag_set_highest_priority (GtkTextTag    *tag,
                               GtkTextBuffer *buffer)
{
	GtkTextTagTable *table;
	gint n;

	table = gtk_text_buffer_get_tag_table (buffer);
	n = gtk_text_tag_table_get_size (table);
	gtk_text_tag_set_priority (tag, n - 1);
}

/* Sets @start and @end to the first non-empty subregion.
 * Returns FALSE if the region is empty.
 */
static gboolean
get_first_subregion (GtkSourceRegion *region,
                     GtkTextIter     *start,
                     GtkTextIter     *end)
{
	GtkSourceRegionIter region_iter;

	if (region == NULL)
	{
		return FALSE;
	}

	gtk_source_region_get_start_region_iter (region, &region_iter);

	while (!gtk_source_region_iter_is_end (&region_iter))
	{
		if (!gtk_source_region_iter_get_subregion (&region_iter, start, end))
		{
			return FALSE;
		}

		if (!gtk_text_iter_equal (start, end))
		{
			return TRUE;
		}

		gtk_source_region_iter_next (&region_iter);
	}

	return FALSE;
}

/* Sets @start and @end to the last non-empty subregion.
 * Returns FALSE if the region is empty.
 */
static gboolean
get_last_subregion (GtkSourceRegion *region,
                    GtkTextIter     *start,
                    GtkTextIter     *end)
{
	GtkSourceRegionIter region_iter;
	gboolean found = FALSE;

	if (region == NULL)
	{
		return FALSE;
	}

	gtk_source_region_get_start_region_iter (region, &region_iter);

	while (!gtk_source_region_iter_is_end (&region_iter))
	{
		GtkTextIter start_subregion;
		GtkTextIter end_subregion;

		if (!gtk_source_region_iter_get_subregion (&region_iter,
							   &start_subregion,
							   &end_subregion))
		{
			return FALSE;
		}

		if (!gtk_text_iter_equal (&start_subregion, &end_subregion))
		{
			found = TRUE;
			*start = start_subregion;
			*end = end_subregion;
		}

		gtk_source_region_iter_next (&region_iter);
	}

	return found;
}

static void
clear_task (GtkSourceSearchContext *search)
{
	g_clear_object (&search->task_region);

	if (search->task != NULL)
	{
		GCancellable *cancellable = g_task_get_cancellable (search->task);

		if (cancellable != NULL)
		{
			g_cancellable_cancel (cancellable);
			g_task_return_error_if_cancelled (search->task);
		}

		g_clear_object (&search->task);
	}
}

static void
clear_search (GtkSourceSearchContext *search)
{
	g_clear_object (&search->scan_region);
	g_clear_object (&search->high_priority_region);

	if (search->idle_scan_id != 0)
	{
		g_source_remove (search->idle_scan_id);
		search->idle_scan_id = 0;
	}

	if (search->regex_error != NULL)
	{
		g_clear_error (&search->regex_error);
		g_object_notify_by_pspec (G_OBJECT (search), properties [PROP_REGEX_ERROR]);
	}

	clear_task (search);

	search->occurrences_count = 0;
}

static GtkTextSearchFlags
get_text_search_flags (GtkSourceSearchContext *search)
{
	GtkTextSearchFlags flags = GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY;

	if (!gtk_source_search_settings_get_case_sensitive (search->settings))
	{
		flags |= GTK_TEXT_SEARCH_CASE_INSENSITIVE;
	}

	return flags;
}

/* @start_pos is in bytes. */
static void
regex_search_get_real_start (GtkSourceSearchContext *search,
                             const GtkTextIter      *start,
                             GtkTextIter            *real_start,
                             gint                   *start_pos)
{
	gint max_lookbehind = impl_regex_get_max_lookbehind (search->regex);
	gint i;
	gchar *text;

	*real_start = *start;

	for (i = 0; i < max_lookbehind; i++)
	{
		if (!gtk_text_iter_backward_char (real_start))
		{
			break;
		}
	}

	text = gtk_text_iter_get_visible_text (real_start, start);
	*start_pos = strlen (text);

	g_free (text);
}

static GRegexMatchFlags
regex_search_get_match_options (const GtkTextIter *real_start,
                                const GtkTextIter *end)
{
	GRegexMatchFlags match_options = 0;

	if (!gtk_text_iter_starts_line (real_start))
	{
		match_options |= G_REGEX_MATCH_NOTBOL;
	}

	if (!gtk_text_iter_ends_line (end))
	{
		match_options |= G_REGEX_MATCH_NOTEOL;
	}

	if (!gtk_text_iter_is_end (end))
	{
		match_options |= G_REGEX_MATCH_PARTIAL_HARD;
	}

	return match_options;
}

/* Get the @match_start and @match_end iters of the @match_info.
 * impl_match_info_fetch_pos() returns byte positions. To get the iters, we need to
 * know the number of UTF-8 characters. A ImplMatchInfo can contain several matches
 * (with impl_match_info_next()). So instead of calling g_utf8_strlen() each time
 * at the beginning of @subject, @iter and @iter_byte_pos are used to remember
 * where g_utf8_strlen() stopped.
 */
static gboolean
regex_search_fetch_match (ImplMatchInfo *match_info,
                          const gchar   *subject,
                          gssize         subject_length,
                          GtkTextIter   *iter,
                          gint          *iter_byte_pos,
                          GtkTextIter   *match_start,
                          GtkTextIter   *match_end)
{
	gint start_byte_pos = 0;
	gint end_byte_pos = 0;
	gint nb_chars;

	g_assert (*iter_byte_pos <= subject_length);
	g_assert (match_start != NULL);
	g_assert (match_end != NULL);

	if (!impl_match_info_matches (match_info))
	{
		return FALSE;
	}

	if (!impl_match_info_fetch_pos (match_info, 0, &start_byte_pos, &end_byte_pos))
	{
		g_warning ("Impossible to fetch regex match position.");
		return FALSE;
	}

	g_assert (start_byte_pos < subject_length);
	g_assert (end_byte_pos <= subject_length);
	g_assert (*iter_byte_pos <= start_byte_pos);
	g_assert (start_byte_pos < end_byte_pos);

	nb_chars = g_utf8_strlen (subject + *iter_byte_pos,
				  start_byte_pos - *iter_byte_pos);

	*match_start = *iter;
	gtk_text_iter_forward_chars (match_start, nb_chars);

	nb_chars = g_utf8_strlen (subject + start_byte_pos,
				  end_byte_pos - start_byte_pos);

	*match_end = *match_start;
	gtk_text_iter_forward_chars (match_end, nb_chars);

	*iter = *match_end;
	*iter_byte_pos = end_byte_pos;

	return TRUE;
}

/* If you retrieve only [match_start, match_end] from the GtkTextBuffer, it
 * does not match the regex if the regex contains look-ahead assertions. For
 * that, get the @real_end. Note that [match_start, real_end] is not the minimum
 * amount of text that still matches the regex, it can contain several
 * occurrences, so you can add the G_REGEX_MATCH_ANCHORED option to match only
 * the first occurrence.
 * Note that @limit is the limit for @match_end, not @real_end.
 */
static gboolean
basic_forward_regex_search (GtkSourceSearchContext *search,
                            const GtkTextIter      *start_at,
                            GtkTextIter            *match_start,
                            GtkTextIter            *match_end,
                            GtkTextIter            *real_end,
                            const GtkTextIter      *limit)
{
	GtkTextIter real_start;
	GtkTextIter end;
	gint start_pos;
	gboolean found = FALSE;
	gint nb_lines = 1;

	if (search->regex == NULL ||
	    search->regex_error != NULL)
	{
		return FALSE;
	}

	regex_search_get_real_start (search, start_at, &real_start, &start_pos);

	if (limit == NULL)
	{
		gtk_text_buffer_get_end_iter (search->buffer, &end);
	}
	else
	{
		end = *limit;
	}

	while (TRUE)
	{
		GRegexMatchFlags match_options;
		gchar *subject;
		gssize subject_length;
		ImplMatchInfo *match_info;
		GtkTextIter iter;
		gint iter_byte_pos;
		GtkTextIter m_start;
		GtkTextIter m_end;

		match_options = regex_search_get_match_options (&real_start, &end);
		subject = gtk_text_iter_get_visible_text (&real_start, &end);
		subject_length = strlen (subject);

		impl_regex_match_full (search->regex,
		                       subject,
		                       subject_length,
		                       start_pos,
		                       match_options,
		                       &match_info,
		                       &search->regex_error);

		iter = real_start;
		iter_byte_pos = 0;

		found = regex_search_fetch_match (match_info,
						  subject,
						  subject_length,
						  &iter,
						  &iter_byte_pos,
						  &m_start,
						  &m_end);

		if (!found && impl_match_info_is_partial_match (match_info))
		{
			gtk_text_iter_forward_lines (&end, nb_lines);
			nb_lines <<= 1;

			g_free (subject);
			impl_match_info_free (match_info);
			continue;
		}

		/* Check that the match is not beyond the limit. This can happen
		 * if a partial match is found on the first iteration. Then the
		 * partial match was actually not a good match, but a second
		 * good match is found.
		 */
		if (found && limit != NULL && gtk_text_iter_compare (limit, &m_end) < 0)
		{
			found = FALSE;
		}

		if (search->regex_error != NULL)
		{
			g_object_notify_by_pspec (G_OBJECT (search), properties [PROP_REGEX_ERROR]);
			found = FALSE;
		}

		if (found)
		{
			if (match_start != NULL)
			{
				*match_start = m_start;
			}

			if (match_end != NULL)
			{
				*match_end = m_end;
			}

			if (real_end != NULL)
			{
				*real_end = end;
			}
		}

		g_free (subject);
		impl_match_info_free (match_info);
		break;
	}

	return found;
}

static gboolean
basic_forward_search (GtkSourceSearchContext *search,
                      const GtkTextIter      *iter,
                      GtkTextIter            *match_start,
                      GtkTextIter            *match_end,
                      const GtkTextIter      *limit)
{
	GtkTextIter begin_search = *iter;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);
	GtkTextSearchFlags flags;

	if (search_text == NULL)
	{
		return FALSE;
	}

	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		return basic_forward_regex_search (search,
						   iter,
						   match_start,
						   match_end,
						   NULL,
						   limit);
	}

	flags = get_text_search_flags (search);

	while (TRUE)
	{
		gboolean found = gtk_text_iter_forward_search (&begin_search,
							       search_text,
							       flags,
							       match_start,
							       match_end,
							       limit);

		if (!found || !gtk_source_search_settings_get_at_word_boundaries (search->settings))
		{
			return found;
		}

		if (_gtk_source_iter_starts_extra_natural_word (match_start, FALSE) &&
		    _gtk_source_iter_ends_extra_natural_word (match_end, FALSE))
		{
			return TRUE;
		}

		begin_search = *match_end;
	}
}

/* We fake the backward regex search by doing a forward search, and taking the
 * last match.
 */
static gboolean
basic_backward_regex_search (GtkSourceSearchContext *search,
                             const GtkTextIter      *start_at,
                             GtkTextIter            *match_start,
                             GtkTextIter            *match_end,
                             const GtkTextIter      *limit)
{
	GtkTextIter lower_bound;
	GtkTextIter m_start;
	GtkTextIter m_end;
	gboolean found = FALSE;

	if (search->regex == NULL ||
	    search->regex_error != NULL)
	{
		return FALSE;
	}

	if (limit == NULL)
	{
		gtk_text_buffer_get_start_iter (search->buffer, &lower_bound);
	}
	else
	{
		lower_bound = *limit;
	}

	while (basic_forward_regex_search (search, &lower_bound, &m_start, &m_end, NULL, start_at))
	{
		found = TRUE;

		if (match_start != NULL)
		{
			*match_start = m_start;
		}

		if (match_end != NULL)
		{
			*match_end = m_end;
		}

		lower_bound = m_end;
	}

	return found;
}

static gboolean
basic_backward_search (GtkSourceSearchContext *search,
                       const GtkTextIter      *iter,
                       GtkTextIter            *match_start,
                       GtkTextIter            *match_end,
                       const GtkTextIter      *limit)
{
	GtkTextIter begin_search = *iter;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);
	GtkTextSearchFlags flags;

	if (search_text == NULL)
	{
		return FALSE;
	}

	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		return basic_backward_regex_search (search,
						    iter,
						    match_start,
						    match_end,
						    limit);
	}

	flags = get_text_search_flags (search);

	while (TRUE)
	{
		gboolean found = gtk_text_iter_backward_search (&begin_search,
							        search_text,
							        flags,
							        match_start,
							        match_end,
							        limit);

		if (!found || !gtk_source_search_settings_get_at_word_boundaries (search->settings))
		{
			return found;
		}

		if (_gtk_source_iter_starts_extra_natural_word (match_start, FALSE) &&
		    _gtk_source_iter_ends_extra_natural_word (match_end, FALSE))
		{
			return TRUE;
		}

		begin_search = *match_start;
	}
}

static void
forward_backward_data_free (ForwardBackwardData *data)
{
	if (data->start_at != NULL)
	{
		GtkTextBuffer *buffer = gtk_text_mark_get_buffer (data->start_at);
		gtk_text_buffer_delete_mark (buffer, data->start_at);
	}

	if (data->match_start != NULL)
	{
		GtkTextBuffer *buffer = gtk_text_mark_get_buffer (data->match_start);
		gtk_text_buffer_delete_mark (buffer, data->match_start);
	}

	if (data->match_end != NULL)
	{
		GtkTextBuffer *buffer = gtk_text_mark_get_buffer (data->match_end);
		gtk_text_buffer_delete_mark (buffer, data->match_end);
	}

	g_slice_free (ForwardBackwardData, data);
}

/* Returns TRUE if finished. */
static gboolean
smart_forward_search_async_step (GtkSourceSearchContext *search,
                                 GtkTextIter            *start_at,
                                 gboolean               *wrapped_around)
{
	GtkTextIter iter = *start_at;
	GtkTextIter limit;
	GtkTextIter region_start = *start_at;
	GtkSourceRegion *region = NULL;
	ForwardBackwardData *task_data;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	if (gtk_text_iter_is_end (start_at))
	{
		if (search_text != NULL &&
		    !*wrapped_around &&
		    gtk_source_search_settings_get_wrap_around (search->settings))
		{
			gtk_text_buffer_get_start_iter (search->buffer, start_at);
			*wrapped_around = TRUE;
			return FALSE;
		}

		task_data = g_slice_new0 (ForwardBackwardData);
		task_data->found = FALSE;
		task_data->is_forward = TRUE;
		task_data->wrapped_around = *wrapped_around;

		g_task_return_pointer (search->task,
				       task_data,
				       (GDestroyNotify)forward_backward_data_free);

		g_clear_object (&search->task);
		return TRUE;
	}

	if (!gtk_text_iter_has_tag (&iter, search->found_tag))
	{
		gtk_text_iter_forward_to_tag_toggle (&iter, search->found_tag);
	}
	else if (!gtk_text_iter_starts_tag (&iter, search->found_tag))
	{
		gtk_text_iter_backward_to_tag_toggle (&iter, search->found_tag);
		region_start = iter;
	}

	limit = iter;
	gtk_text_iter_forward_to_tag_toggle (&limit, search->found_tag);

	if (search->scan_region != NULL)
	{
		region = gtk_source_region_intersect_subregion (search->scan_region,
								&region_start,
								&limit);
	}

	if (gtk_source_region_is_empty (region))
	{
		GtkTextIter match_start;
		GtkTextIter match_end;

		g_clear_object (&region);

		while (basic_forward_search (search, &iter, &match_start, &match_end, &limit))
		{
			if (gtk_text_iter_compare (&match_start, start_at) < 0)
			{
				iter = match_end;
				continue;
			}

			task_data = g_slice_new0 (ForwardBackwardData);
			task_data->found = TRUE;
			task_data->match_start =
                                gtk_text_buffer_create_mark (search->buffer,
                                                             NULL,
                                                             &match_start,
                                                             TRUE);
			task_data->match_end =
                                gtk_text_buffer_create_mark (search->buffer,
                                                             NULL,
                                                             &match_end,
                                                             FALSE);
			task_data->is_forward = TRUE;
			task_data->wrapped_around = *wrapped_around;

			g_task_return_pointer (search->task,
					       task_data,
					       (GDestroyNotify)forward_backward_data_free);

			g_clear_object (&search->task);
			return TRUE;
		}

		*start_at = limit;
		return FALSE;
	}

	task_data = g_slice_new0 (ForwardBackwardData);
	task_data->is_forward = TRUE;
	task_data->wrapped_around = *wrapped_around;
	task_data->start_at = gtk_text_buffer_create_mark (search->buffer,
							   NULL,
							   start_at,
							   TRUE);

	g_task_set_task_data (search->task,
			      task_data,
			      (GDestroyNotify)forward_backward_data_free);

	g_clear_object (&search->task_region);
	search->task_region = region;

	install_idle_scan (search);

	/* The idle that scan the task region will call
	 * smart_forward_search_async() to continue the task. But for the
	 * moment, we are done.
	 */
	return TRUE;
}

static void
smart_forward_search_async (GtkSourceSearchContext *search,
                            const GtkTextIter      *start_at,
                            gboolean                wrapped_around)
{
	GtkTextIter iter = *start_at;

	/* A recursive function would have been more natural, but a loop is
	 * better to avoid stack overflows.
	 */
	while (!smart_forward_search_async_step (search, &iter, &wrapped_around));
}


/* Returns TRUE if finished. */
static gboolean
smart_backward_search_async_step (GtkSourceSearchContext *search,
                                  GtkTextIter            *start_at,
                                  gboolean               *wrapped_around)
{
	GtkTextIter iter = *start_at;
	GtkTextIter limit;
	GtkTextIter region_end = *start_at;
	GtkSourceRegion *region = NULL;
	ForwardBackwardData *task_data;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	if (gtk_text_iter_is_start (start_at))
	{
		if (search_text != NULL &&
		    !*wrapped_around &&
		    gtk_source_search_settings_get_wrap_around (search->settings))
		{
			gtk_text_buffer_get_end_iter (search->buffer, start_at);
			*wrapped_around = TRUE;
			return FALSE;
		}

		task_data = g_slice_new0 (ForwardBackwardData);
		task_data->found = FALSE;
		task_data->is_forward = FALSE;
		task_data->wrapped_around = *wrapped_around;

		g_task_return_pointer (search->task,
				       task_data,
				       (GDestroyNotify)forward_backward_data_free);

		g_clear_object (&search->task);
		return TRUE;
	}

	if (gtk_text_iter_starts_tag (&iter, search->found_tag) ||
	    (!gtk_text_iter_has_tag (&iter, search->found_tag) &&
	     !gtk_text_iter_ends_tag (&iter, search->found_tag)))
	{
		gtk_text_iter_backward_to_tag_toggle (&iter, search->found_tag);
	}
	else if (gtk_text_iter_has_tag (&iter, search->found_tag))
	{
		gtk_text_iter_forward_to_tag_toggle (&iter, search->found_tag);
		region_end = iter;
	}

	limit = iter;
	gtk_text_iter_backward_to_tag_toggle (&limit, search->found_tag);

	if (search->scan_region != NULL)
	{
		region = gtk_source_region_intersect_subregion (search->scan_region,
								&limit,
								&region_end);
	}

	if (gtk_source_region_is_empty (region))
	{
		GtkTextIter match_start;
		GtkTextIter match_end;

		g_clear_object (&region);

		while (basic_backward_search (search, &iter, &match_start, &match_end, &limit))
		{
			if (gtk_text_iter_compare (start_at, &match_end) < 0)
			{
				iter = match_start;
				continue;
			}

			task_data = g_slice_new0 (ForwardBackwardData);
			task_data->found = TRUE;
			task_data->match_start =
				gtk_text_buffer_create_mark (search->buffer,
				                             NULL,
				                             &match_start,
				                             TRUE);
			task_data->match_end =
				gtk_text_buffer_create_mark (search->buffer,
				                             NULL,
				                             &match_end,
				                             FALSE);
			task_data->is_forward = FALSE;
			task_data->wrapped_around = *wrapped_around;

			g_task_return_pointer (search->task,
					       task_data,
					       (GDestroyNotify)forward_backward_data_free);

			g_clear_object (&search->task);
			return TRUE;
		}

		*start_at = limit;
		return FALSE;
	}

	task_data = g_slice_new0 (ForwardBackwardData);
	task_data->is_forward = FALSE;
	task_data->wrapped_around = *wrapped_around;
	task_data->start_at = gtk_text_buffer_create_mark (search->buffer,
							   NULL,
							   start_at,
							   TRUE);

	g_task_set_task_data (search->task,
			      task_data,
			      (GDestroyNotify)forward_backward_data_free);

	g_clear_object (&search->task_region);
	search->task_region = region;

	install_idle_scan (search);

	/* The idle that scan the task region will call
	 * smart_backward_search_async() to continue the task. But for the
	 * moment, we are done.
	 */
	return TRUE;
}

static void
smart_backward_search_async (GtkSourceSearchContext *search,
                             const GtkTextIter      *start_at,
                             gboolean                wrapped_around)
{
	GtkTextIter iter = *start_at;

	/* A recursive function would have been more natural, but a loop is
	 * better to avoid stack overflows.
	 */
	while (!smart_backward_search_async_step (search, &iter, &wrapped_around));
}

/* Adjust the subregion so we are sure that all matches that are visible or
 * partially visible between @start and @end are highlighted.
 */
static void
adjust_subregion (GtkSourceSearchContext *search,
                  GtkTextIter            *start,
                  GtkTextIter            *end)
{
	DEBUG ({
		g_print ("adjust_subregion(), before adjusting: [%u (%u), %u (%u)]\n",
			 gtk_text_iter_get_line (start), gtk_text_iter_get_offset (start),
			 gtk_text_iter_get_line (end), gtk_text_iter_get_offset (end));
	});

	gtk_text_iter_backward_lines (start, MAX (0, search->text_nb_lines - 1));
	gtk_text_iter_forward_lines (end, MAX (0, search->text_nb_lines - 1));

	if (!gtk_text_iter_starts_line (start))
	{
		gtk_text_iter_set_line_offset (start, 0);
	}

	if (!gtk_text_iter_ends_line (end))
	{
		gtk_text_iter_forward_to_line_end (end);
	}

	/* When we are in the middle of a found_tag, a simple solution is to
	 * always backward_to_tag_toggle(). The problem is that occurrences can
	 * be contiguous. So a full scan of the buffer can have a O(n^2) in the
	 * worst case, if we use the simple solution. Therefore we use a more
	 * complicated solution, that checks if we are in an old found_tag or
	 * not.
	 */

	if (gtk_text_iter_has_tag (start, search->found_tag))
	{
		if (gtk_source_region_is_empty (search->scan_region))
		{
			/* 'start' is in a correct match, we can skip it. */
			gtk_text_iter_forward_to_tag_toggle (start, search->found_tag);
		}
		else
		{
			GtkTextIter tag_start = *start;
			GtkTextIter tag_end = *start;
			GtkSourceRegion *region;

			if (!gtk_text_iter_starts_tag (&tag_start, search->found_tag))
			{
				gtk_text_iter_backward_to_tag_toggle (&tag_start, search->found_tag);
			}

			gtk_text_iter_forward_to_tag_toggle (&tag_end, search->found_tag);

			region = gtk_source_region_intersect_subregion (search->scan_region,
									&tag_start,
									&tag_end);

			if (gtk_source_region_is_empty (region))
			{
				/* 'region' has already been scanned, so 'start' is in a
				 * correct match, we can skip it.
				 */
				*start = tag_end;
			}
			else
			{
				/* 'region' has not already been scanned completely, so
				 * 'start' is most probably in an old match that must be
				 * removed.
				 */
				*start = tag_start;
			}

			g_clear_object (&region);
		}
	}

	/* Symmetric for 'end'. */

	if (gtk_text_iter_has_tag (end, search->found_tag))
	{
		if (gtk_source_region_is_empty (search->scan_region))
		{
			/* 'end' is in a correct match, we can skip it. */

			if (!gtk_text_iter_starts_tag (end, search->found_tag))
			{
				gtk_text_iter_backward_to_tag_toggle (end, search->found_tag);
			}
		}
		else
		{
			GtkTextIter tag_start = *end;
			GtkTextIter tag_end = *end;
			GtkSourceRegion *region;

			if (!gtk_text_iter_starts_tag (&tag_start, search->found_tag))
			{
				gtk_text_iter_backward_to_tag_toggle (&tag_start, search->found_tag);
			}

			gtk_text_iter_forward_to_tag_toggle (&tag_end, search->found_tag);

			region = gtk_source_region_intersect_subregion (search->scan_region,
									&tag_start,
									&tag_end);

			if (gtk_source_region_is_empty (region))
			{
				/* 'region' has already been scanned, so 'end' is in a
				 * correct match, we can skip it.
				 */
				*end = tag_start;
			}
			else
			{
				/* 'region' has not already been scanned completely, so
				 * 'end' is most probably in an old match that must be
				 * removed.
				 */
				*end = tag_end;
			}

			g_clear_object (&region);
		}
	}

	DEBUG ({
		g_print ("adjust_subregion(), after adjusting: [%u (%u), %u (%u)]\n",
			 gtk_text_iter_get_line (start), gtk_text_iter_get_offset (start),
			 gtk_text_iter_get_line (end), gtk_text_iter_get_offset (end));
	});
}

/* Do not take into account the scan_region. Take the result with a grain of
 * salt. You should verify before or after calling this function that the
 * region has been scanned, to be sure that the returned occurrence is correct.
 */
static gboolean
smart_forward_search_without_scanning (GtkSourceSearchContext *search,
                                       const GtkTextIter      *start_at,
                                       GtkTextIter            *match_start,
                                       GtkTextIter            *match_end,
                                       const GtkTextIter      *stop_at)
{
	GtkTextIter iter;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	g_assert (start_at != NULL);
	g_assert (match_start != NULL);
	g_assert (match_end != NULL);
	g_assert (stop_at != NULL);

	iter = *start_at;

	if (search_text == NULL)
	{
		return FALSE;
	}

	while (gtk_text_iter_compare (&iter, stop_at) < 0)
	{
		GtkTextIter limit;

		if (!gtk_text_iter_has_tag (&iter, search->found_tag))
		{
			gtk_text_iter_forward_to_tag_toggle (&iter, search->found_tag);
		}
		else if (!gtk_text_iter_starts_tag (&iter, search->found_tag))
		{
			gtk_text_iter_backward_to_tag_toggle (&iter, search->found_tag);
		}

		limit = iter;
		gtk_text_iter_forward_to_tag_toggle (&limit, search->found_tag);

		if (gtk_text_iter_compare (stop_at, &limit) < 0)
		{
			limit = *stop_at;
		}

		while (basic_forward_search (search, &iter, match_start, match_end, &limit))
		{
			if (gtk_text_iter_compare (start_at, match_start) <= 0)
			{
				return TRUE;
			}

			iter = *match_end;
		}

		iter = limit;
	}

	return FALSE;
}

/* Remove the occurrences in the range. @start and @end may be adjusted, if they
 * are in a found_tag region.
 */
static void
remove_occurrences_in_range (GtkSourceSearchContext *search,
                             GtkTextIter            *start,
                             GtkTextIter            *end)
{
	GtkTextIter iter;
	GtkTextIter match_start;
	GtkTextIter match_end;

	if ((gtk_text_iter_has_tag (start, search->found_tag) &&
	     !gtk_text_iter_starts_tag (start, search->found_tag)) ||
	    (gtk_source_search_settings_get_at_word_boundaries (search->settings) &&
	     gtk_text_iter_ends_tag (start, search->found_tag)))
	{
		gtk_text_iter_backward_to_tag_toggle (start, search->found_tag);
	}

	if ((gtk_text_iter_has_tag (end, search->found_tag) &&
	     !gtk_text_iter_starts_tag (end, search->found_tag)) ||
	    (gtk_source_search_settings_get_at_word_boundaries (search->settings) &&
	     gtk_text_iter_starts_tag (end, search->found_tag)))
	{
		gtk_text_iter_forward_to_tag_toggle (end, search->found_tag);
	}

	iter = *start;

	while (smart_forward_search_without_scanning (search, &iter, &match_start, &match_end, end))
	{
		if (search->scan_region == NULL)
		{
			/* The occurrence has already been scanned, and thus
			 * occurrence_count take it into account. */
			search->occurrences_count--;
		}
		else
		{
			GtkSourceRegion *region;

			region = gtk_source_region_intersect_subregion (search->scan_region,
									&match_start,
									&match_end);

			if (gtk_source_region_is_empty (region))
			{
				search->occurrences_count--;
			}

			g_clear_object (&region);
		}

		iter = match_end;
	}

	gtk_text_buffer_remove_tag (search->buffer,
				    search->found_tag,
				    start,
				    end);
}

static void
scan_subregion (GtkSourceSearchContext *search,
                GtkTextIter            *start,
                GtkTextIter            *end)
{
	GtkTextIter iter;
	GtkTextIter *limit;
	gboolean found = TRUE;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	/* Make sure the 'found' tag has the priority over syntax highlighting
	 * tags. */
	text_tag_set_highest_priority (search->found_tag,
				       search->buffer);

	adjust_subregion (search, start, end);
	remove_occurrences_in_range (search, start, end);

	if (search->scan_region != NULL)
	{
		DEBUG ({
			g_print ("Region to scan, before:\n");
			print_region (search->scan_region);
		});

		gtk_source_region_subtract_subregion (search->scan_region, start, end);

		DEBUG ({
			g_print ("Region to scan, after:\n");
			print_region (search->scan_region);
		});
	}

	if (search->task_region != NULL)
	{
		gtk_source_region_subtract_subregion (search->task_region, start, end);
	}

	if (search_text == NULL)
	{
		/* We have removed the found_tag, we are done. */
		return;
	}

	iter = *start;

	if (gtk_text_iter_is_end (end))
	{
		limit = NULL;
	}
	else
	{
		limit = end;
	}

	do
	{
		GtkTextIter match_start;
		GtkTextIter match_end;

		found = basic_forward_search (search, &iter, &match_start, &match_end, limit);

		if (found)
		{
			gtk_text_buffer_apply_tag (search->buffer,
						   search->found_tag,
						   &match_start,
						   &match_end);

			search->occurrences_count++;
		}

		iter = match_end;

	} while (found);
}

static void
scan_all_region (GtkSourceSearchContext *search,
                 GtkSourceRegion        *region)
{
	GtkSourceRegionIter region_iter;

	gtk_source_region_get_start_region_iter (region, &region_iter);

	while (!gtk_source_region_iter_is_end (&region_iter))
	{
		GtkTextIter subregion_start;
		GtkTextIter subregion_end;

		if (!gtk_source_region_iter_get_subregion (&region_iter,
							   &subregion_start,
							   &subregion_end))
		{
			break;
		}

		scan_subregion (search,
				&subregion_start,
				&subregion_end);

		gtk_source_region_iter_next (&region_iter);
	}
}

/* Scan a chunk of the region. If the region is small enough, all the region
 * will be scanned. But if the region is big, scanning only the chunk will not
 * block the UI normally. Begin the scan at the beginning of the region.
 */
static void
scan_region_forward (GtkSourceSearchContext *search,
                     GtkSourceRegion        *region)
{
	gint nb_remaining_lines = SCAN_BATCH_SIZE;
	GtkTextIter start;
	GtkTextIter end;

	while (nb_remaining_lines > 0 &&
	       get_first_subregion (region, &start, &end))
	{
		GtkTextIter limit = start;
		gint start_line;
		gint limit_line;

		gtk_text_iter_forward_lines (&limit, nb_remaining_lines);

		if (gtk_text_iter_compare (&end, &limit) < 0)
		{
			limit = end;
		}

		scan_subregion (search, &start, &limit);

		gtk_source_region_subtract_subregion (region, &start, &limit);

		start_line = gtk_text_iter_get_line (&start);
		limit_line = gtk_text_iter_get_line (&limit);

		nb_remaining_lines -= limit_line - start_line;
	}
}

/* Same as scan_region_forward(), but begins the scan at the end of the region. */
static void
scan_region_backward (GtkSourceSearchContext *search,
                      GtkSourceRegion        *region)
{
	gint nb_remaining_lines = SCAN_BATCH_SIZE;
	GtkTextIter start;
	GtkTextIter end;

	while (nb_remaining_lines > 0 &&
	       get_last_subregion (region, &start, &end))
	{
		GtkTextIter limit = end;
		gint limit_line;
		gint end_line;

		gtk_text_iter_backward_lines (&limit, nb_remaining_lines);

		if (gtk_text_iter_compare (&limit, &start) < 0)
		{
			limit = start;
		}

		scan_subregion (search, &limit, &end);

		gtk_source_region_subtract_subregion (region, &limit, &end);

		limit_line = gtk_text_iter_get_line (&limit);
		end_line = gtk_text_iter_get_line (&end);

		nb_remaining_lines -= end_line - limit_line;
	}
}

static void
resume_task (GtkSourceSearchContext *search)
{
	ForwardBackwardData *task_data = g_task_get_task_data (search->task);
	GtkTextIter start_at;

	g_clear_object (&search->task_region);

	gtk_text_buffer_get_iter_at_mark (search->buffer,
					  &start_at,
					  task_data->start_at);

	if (task_data->is_forward)
	{
		smart_forward_search_async (search,
					    &start_at,
					    task_data->wrapped_around);
	}
	else
	{
		smart_backward_search_async (search,
					     &start_at,
					     task_data->wrapped_around);
	}
}

static void
scan_task_region (GtkSourceSearchContext *search)
{
	ForwardBackwardData *task_data = g_task_get_task_data (search->task);

	if (task_data->is_forward)
	{
		scan_region_forward (search, search->task_region);
	}
	else
	{
		scan_region_backward (search, search->task_region);
	}

	resume_task (search);
}

static gboolean
idle_scan_normal_search (GtkSourceSearchContext *search)
{
	if (search->high_priority_region != NULL)
	{
		/* Normally the high priority region is not really big, since it
		 * is the visible area on the screen. So we can highlight it in
		 * one batch.
		 */
		scan_all_region (search, search->high_priority_region);

		g_clear_object (&search->high_priority_region);
		return G_SOURCE_CONTINUE;
	}

	if (search->task_region != NULL)
	{
		scan_task_region (search);
		return G_SOURCE_CONTINUE;
	}

	scan_region_forward (search, search->scan_region);

	if (gtk_source_region_is_empty (search->scan_region))
	{
		search->idle_scan_id = 0;

		g_object_notify_by_pspec (G_OBJECT (search),
		                          properties [PROP_OCCURRENCES_COUNT]);

		g_clear_object (&search->scan_region);
		return G_SOURCE_REMOVE;
	}

	return G_SOURCE_CONTINUE;
}

/* Just remove the found_tag's located in the high-priority region. For big
 * documents, if the pattern is modified, it can take some time to re-scan all
 * the buffer, so it's better to clear the highlighting as soon as possible. If
 * the highlighting is not cleared, the user can wrongly think that the new
 * pattern matches the old occurrences.
 * The drawback of clearing the highlighting is that for small documents, there
 * is some flickering.
 */
static void
regex_search_handle_high_priority_region (GtkSourceSearchContext *search)
{
	GtkSourceRegion *region;
	GtkSourceRegionIter region_iter;

	region = gtk_source_region_intersect_region (search->high_priority_region,
						     search->scan_region);

	if (region == NULL)
	{
		return;
	}

	gtk_source_region_get_start_region_iter (region, &region_iter);

	while (!gtk_source_region_iter_is_end (&region_iter))
	{
		GtkTextIter subregion_start;
		GtkTextIter subregion_end;

		if (!gtk_source_region_iter_get_subregion (&region_iter,
							   &subregion_start,
							   &subregion_end))
		{
			break;
		}

		gtk_text_buffer_remove_tag (search->buffer,
					    search->found_tag,
					    &subregion_start,
					    &subregion_end);

		gtk_source_region_iter_next (&region_iter);
	}

	g_clear_object (&region);
}

/* Returns TRUE if the segment is finished, and FALSE on partial match. */
static gboolean
regex_search_scan_segment (GtkSourceSearchContext *search,
                           const GtkTextIter      *segment_start,
                           const GtkTextIter      *segment_end,
                           GtkTextIter            *stopped_at)
{
	GtkTextIter real_start;
	gint start_pos;
	gchar *subject;
	gssize subject_length;
	GRegexMatchFlags match_options;
	ImplMatchInfo *match_info;
	GtkTextIter iter;
	gint iter_byte_pos;
	gboolean segment_finished;
	GtkTextIter match_start;
	GtkTextIter match_end;

	g_assert (stopped_at != NULL);

	gtk_text_buffer_remove_tag (search->buffer,
				    search->found_tag,
				    segment_start,
				    segment_end);

	if (search->regex == NULL ||
	    search->regex_error != NULL)
	{
		*stopped_at = *segment_end;
		return TRUE;
	}

	regex_search_get_real_start (search,
				     segment_start,
				     &real_start,
				     &start_pos);

	DEBUG ({
	       g_print ("\n*** regex search - scan segment ***\n");
	       g_print ("start position in the subject (in bytes): %d\n", start_pos);
	});

	match_options = regex_search_get_match_options (&real_start, segment_end);

	if (match_options & G_REGEX_MATCH_NOTBOL)
	{
		DEBUG ({
		       g_print ("match notbol\n");
		});
	}

	if (match_options & G_REGEX_MATCH_NOTEOL)
	{
		DEBUG ({
		       g_print ("match noteol\n");
		});
	}

	if (match_options & G_REGEX_MATCH_PARTIAL_HARD)
	{
		DEBUG ({
		       g_print ("match partial hard\n");
		});
	}

	subject = gtk_text_iter_get_visible_text (&real_start, segment_end);
	subject_length = strlen (subject);

	DEBUG ({
	       gchar *subject_escaped = gtk_source_utils_escape_search_text (subject);
	       g_print ("subject (escaped): %s\n", subject_escaped);
	       g_free (subject_escaped);
	});

	impl_regex_match_full (search->regex,
	                       subject,
	                       subject_length,
	                       start_pos,
	                       match_options,
	                       &match_info,
	                       &search->regex_error);

	iter = real_start;
	iter_byte_pos = 0;

	while (regex_search_fetch_match (match_info,
					 subject,
					 subject_length,
					 &iter,
					 &iter_byte_pos,
					 &match_start,
					 &match_end))
	{
		gtk_text_buffer_apply_tag (search->buffer,
					   search->found_tag,
					   &match_start,
					   &match_end);

		DEBUG ({
			 gchar *match_text = gtk_text_iter_get_visible_text (&match_start, &match_end);
			 gchar *match_escaped = gtk_source_utils_escape_search_text (match_text);
			 g_print ("match found (escaped): %s\n", match_escaped);
			 g_free (match_text);
			 g_free (match_escaped);
		});

		search->occurrences_count++;

		impl_match_info_next (match_info, &search->regex_error);
	}

	if (search->regex_error != NULL)
	{
		g_object_notify_by_pspec (G_OBJECT (search), properties [PROP_REGEX_ERROR]);
	}

	if (impl_match_info_is_partial_match (match_info))
	{
		segment_finished = FALSE;

		if (gtk_text_iter_compare (segment_start, &iter) < 0)
		{
			*stopped_at = iter;
		}
		else
		{
			*stopped_at = *segment_start;
		}

		DEBUG ({
		       g_print ("partial match\n");
		});
	}
	else
	{
		*stopped_at = *segment_end;
		segment_finished = TRUE;
	}

	g_free (subject);
	impl_match_info_free (match_info);

	return segment_finished;
}

static void
regex_search_scan_chunk (GtkSourceSearchContext *search,
                         const GtkTextIter      *chunk_start,
                         const GtkTextIter      *chunk_end)
{
	GtkTextIter segment_start = *chunk_start;

	while (gtk_text_iter_compare (&segment_start, chunk_end) < 0)
	{
		GtkTextIter segment_end;
		GtkTextIter stopped_at;
		gint nb_lines = 1;

		segment_end = segment_start;
		gtk_text_iter_forward_line (&segment_end);

		while (!regex_search_scan_segment (search,
						   &segment_start,
						   &segment_end,
						   &stopped_at))
		{
			/* TODO: performance improvement. On partial match, use
			 * a GString to grow the subject.
			 */

			segment_start = stopped_at;
			gtk_text_iter_forward_lines (&segment_end, nb_lines);
			nb_lines <<= 1;
		}

		segment_start = stopped_at;
	}

	gtk_source_region_subtract_subregion (search->scan_region,
					      chunk_start,
					      &segment_start);

	if (search->task_region != NULL)
	{
		gtk_source_region_subtract_subregion (search->task_region,
						      chunk_start,
						      &segment_start);
	}
}

static void
regex_search_scan_next_chunk (GtkSourceSearchContext *search)
{
	GtkTextIter chunk_start;
	GtkTextIter chunk_end;

	if (gtk_source_region_is_empty (search->scan_region))
	{
		return;
	}

	if (!gtk_source_region_get_bounds (search->scan_region, &chunk_start, NULL))
	{
		return;
	}

	chunk_end = chunk_start;
	gtk_text_iter_forward_lines (&chunk_end, SCAN_BATCH_SIZE);

	regex_search_scan_chunk (search, &chunk_start, &chunk_end);
}

static gboolean
idle_scan_regex_search (GtkSourceSearchContext *search)
{
	if (search->high_priority_region != NULL)
	{
		regex_search_handle_high_priority_region (search);
		g_clear_object (&search->high_priority_region);
		return G_SOURCE_CONTINUE;
	}

	regex_search_scan_next_chunk (search);

	if (search->task != NULL)
	{
		/* Always resume the task, even if the task region has not been
		 * fully scanned. The task region can be huge (the whole
		 * buffer), and an occurrence can be found earlier. Obviously it
		 * would be better to resume the task only if an occurrence has
		 * been found in the task region. But it would be a little more
		 * complicated to implement, for not a big performance
		 * improvement.
		 */
		resume_task (search);
		return G_SOURCE_CONTINUE;
	}

	if (gtk_source_region_is_empty (search->scan_region))
	{
		search->idle_scan_id = 0;

		g_object_notify_by_pspec (G_OBJECT (search),
		                          properties [PROP_OCCURRENCES_COUNT]);

		g_clear_object (&search->scan_region);
		return G_SOURCE_REMOVE;
	}

	return G_SOURCE_CONTINUE;
}

static gboolean
idle_scan_cb (GtkSourceSearchContext *search)
{
	if (search->buffer == NULL)
	{
		search->idle_scan_id = 0;
		clear_search (search);
		return G_SOURCE_REMOVE;
	}

	return gtk_source_search_settings_get_regex_enabled (search->settings) ?
	       idle_scan_regex_search (search) :
	       idle_scan_normal_search (search);
}

static void
install_idle_scan (GtkSourceSearchContext *search)
{
	if (search->idle_scan_id == 0)
	{
		search->idle_scan_id = g_idle_add ((GSourceFunc)idle_scan_cb, search);
	}
}

/* Returns TRUE when finished. */
static gboolean
smart_forward_search_step (GtkSourceSearchContext *search,
                           GtkTextIter            *start_at,
                           GtkTextIter            *match_start,
                           GtkTextIter            *match_end)
{
	GtkTextIter iter = *start_at;
	GtkTextIter limit;
	GtkTextIter region_start = *start_at;
	GtkSourceRegion *region = NULL;

	if (!gtk_text_iter_has_tag (&iter, search->found_tag))
	{
		gtk_text_iter_forward_to_tag_toggle (&iter, search->found_tag);
	}
	else if (!gtk_text_iter_starts_tag (&iter, search->found_tag))
	{
		gtk_text_iter_backward_to_tag_toggle (&iter, search->found_tag);
		region_start = iter;
	}

	limit = iter;
	gtk_text_iter_forward_to_tag_toggle (&limit, search->found_tag);

	if (search->scan_region != NULL)
	{
		region = gtk_source_region_intersect_subregion (search->scan_region,
								&region_start,
								&limit);
	}

	if (gtk_source_region_is_empty (region))
	{
		g_clear_object (&region);

		while (basic_forward_search (search, &iter, match_start, match_end, &limit))
		{
			if (gtk_text_iter_compare (start_at, match_start) <= 0)
			{
				return TRUE;
			}

			iter = *match_end;
		}

		*start_at = limit;
		return FALSE;
	}

	/* Scan a chunk of the buffer, not the whole 'region'. An occurrence can
	 * be found before the 'region' is scanned entirely.
	 */
	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		regex_search_scan_next_chunk (search);
	}
	else
	{
		scan_region_forward (search, region);
	}

	g_clear_object (&region);

	return FALSE;
}

/* Doesn't wrap around. */
static gboolean
smart_forward_search (GtkSourceSearchContext *search,
                      const GtkTextIter      *start_at,
                      GtkTextIter            *match_start,
                      GtkTextIter            *match_end)
{
	GtkTextIter iter = *start_at;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	g_return_val_if_fail (match_start != NULL, FALSE);
	g_return_val_if_fail (match_end != NULL, FALSE);

	if (search_text == NULL)
	{
		return FALSE;
	}

	while (!gtk_text_iter_is_end (&iter))
	{
		if (smart_forward_search_step (search, &iter, match_start, match_end))
		{
			return TRUE;
		}
	}

	return FALSE;
}

/* Returns TRUE when finished. */
static gboolean
smart_backward_search_step (GtkSourceSearchContext *search,
                            GtkTextIter            *start_at,
                            GtkTextIter            *match_start,
                            GtkTextIter            *match_end)
{
	GtkTextIter iter = *start_at;
	GtkTextIter limit;
	GtkTextIter region_end = *start_at;
	GtkSourceRegion *region = NULL;

	if (gtk_text_iter_starts_tag (&iter, search->found_tag) ||
	    (!gtk_text_iter_has_tag (&iter, search->found_tag) &&
	     !gtk_text_iter_ends_tag (&iter, search->found_tag)))
	{
		gtk_text_iter_backward_to_tag_toggle (&iter, search->found_tag);
	}
	else if (gtk_text_iter_has_tag (&iter, search->found_tag))
	{
		gtk_text_iter_forward_to_tag_toggle (&iter, search->found_tag);
		region_end = iter;
	}

	limit = iter;
	gtk_text_iter_backward_to_tag_toggle (&limit, search->found_tag);

	if (search->scan_region != NULL)
	{
		region = gtk_source_region_intersect_subregion (search->scan_region,
								&limit,
								&region_end);
	}

	if (gtk_source_region_is_empty (region))
	{
		g_clear_object (&region);

		while (basic_backward_search (search, &iter, match_start, match_end, &limit))
		{
			if (gtk_text_iter_compare (match_end, start_at) <= 0)
			{
				return TRUE;
			}

			iter = *match_start;
		}

		*start_at = limit;
		return FALSE;
	}

	/* Scan a chunk of the buffer, not the whole 'region'. An occurrence can
	 * be found before the 'region' is scanned entirely.
	 */
	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		regex_search_scan_next_chunk (search);
	}
	else
	{
		scan_region_forward (search, region);
	}

	g_clear_object (&region);

	return FALSE;
}

/* Doesn't wrap around. */
static gboolean
smart_backward_search (GtkSourceSearchContext *search,
                       const GtkTextIter      *start_at,
                       GtkTextIter            *match_start,
                       GtkTextIter            *match_end)
{
	GtkTextIter iter = *start_at;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	g_return_val_if_fail (match_start != NULL, FALSE);
	g_return_val_if_fail (match_end != NULL, FALSE);

	if (search_text == NULL)
	{
		return FALSE;
	}

	while (!gtk_text_iter_is_start (&iter))
	{
		if (smart_backward_search_step (search, &iter, match_start, match_end))
		{
			return TRUE;
		}
	}

	return FALSE;
}

static void
add_subregion_to_scan (GtkSourceSearchContext *search,
                       const GtkTextIter      *subregion_start,
                       const GtkTextIter      *subregion_end)
{
	GtkTextIter start = *subregion_start;
	GtkTextIter end = *subregion_end;

	if (search->scan_region == NULL)
	{
		search->scan_region = gtk_source_region_new (search->buffer);
	}

	DEBUG ({
		g_print ("add_subregion_to_scan(): region to scan, before:\n");
		print_region (search->scan_region);
	});

	gtk_source_region_add_subregion (search->scan_region, &start, &end);

	DEBUG ({
		g_print ("add_subregion_to_scan(): region to scan, after:\n");
		print_region (search->scan_region);
	});

	install_idle_scan (search);
}

static void
update_regex (GtkSourceSearchContext *search)
{
	gboolean regex_error_changed = FALSE;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	if (search->regex != NULL)
	{
		impl_regex_unref (search->regex);
		search->regex = NULL;
	}

	if (search->regex_error != NULL)
	{
		g_clear_error (&search->regex_error);
		regex_error_changed = TRUE;
	}

	if (search_text != NULL &&
	    gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		GRegexCompileFlags compile_flags = G_REGEX_MULTILINE;
		gchar *pattern = (gchar *)search_text;

		search->text_nb_lines = 0;

		if (!gtk_source_search_settings_get_case_sensitive (search->settings))
		{
			compile_flags |= G_REGEX_CASELESS;
		}

		if (gtk_source_search_settings_get_at_word_boundaries (search->settings))
		{
			pattern = g_strdup_printf ("\\b%s\\b", search_text);
		}

		search->regex = impl_regex_new (pattern,
		                                compile_flags,
		                                G_REGEX_MATCH_NOTEMPTY,
		                                &search->regex_error);

		if (search->regex_error != NULL)
		{
			regex_error_changed = TRUE;
		}

		if (gtk_source_search_settings_get_at_word_boundaries (search->settings))
		{
			g_free (pattern);
		}
	}

	if (regex_error_changed)
	{
		g_object_notify_by_pspec (G_OBJECT (search), properties [PROP_REGEX_ERROR]);
	}
}

static void
update (GtkSourceSearchContext *search)
{
	GtkTextIter start;
	GtkTextIter end;
	GtkSourceBufferInternal *buffer_internal;

	if (search->buffer == NULL)
	{
		return;
	}

	clear_search (search);
	update_regex (search);

	search->scan_region = gtk_source_region_new (search->buffer);

	gtk_text_buffer_get_bounds (search->buffer, &start, &end);
	add_subregion_to_scan (search, &start, &end);

	/* Notify the GtkSourceViews that the search is starting, so that
	 * _gtk_source_search_context_update_highlight() can be called for the
	 * visible regions of the buffer.
	 */
	buffer_internal = _gtk_source_buffer_internal_get_from_buffer (GTK_SOURCE_BUFFER (search->buffer));
	_gtk_source_buffer_internal_emit_search_start (buffer_internal, search);
}

static void
insert_text_before_cb (GtkSourceSearchContext *search,
                       GtkTextIter            *location,
                       gchar                  *text,
                       gint                    length)
{
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	clear_task (search);

	if (search_text != NULL &&
	    !gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		GtkTextIter start = *location;
		GtkTextIter end = *location;

		remove_occurrences_in_range (search, &start, &end);
		add_subregion_to_scan (search, &start, &end);
	}
}

static void
insert_text_after_cb (GtkSourceSearchContext *search,
                      GtkTextIter            *location,
                      gchar                  *text,
                      gint                    length)
{
	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		update (search);
	}
	else
	{
		GtkTextIter start;
		GtkTextIter end;

		start = end = *location;

		gtk_text_iter_backward_chars (&start,
					      g_utf8_strlen (text, length));

		add_subregion_to_scan (search, &start, &end);
	}
}

static void
delete_range_before_cb (GtkSourceSearchContext *search,
                        GtkTextIter            *delete_start,
                        GtkTextIter            *delete_end)
{
	GtkTextIter start_buffer;
	GtkTextIter end_buffer;
	const gchar *search_text = gtk_source_search_settings_get_search_text (search->settings);

	clear_task (search);

	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		return;
	}

	gtk_text_buffer_get_bounds (search->buffer, &start_buffer, &end_buffer);

	if (gtk_text_iter_equal (delete_start, &start_buffer) &&
	    gtk_text_iter_equal (delete_end, &end_buffer))
	{
		/* Special case when removing all the text. */
		search->occurrences_count = 0;
		return;
	}

	if (search_text != NULL)
	{
		GtkTextIter start = *delete_start;
		GtkTextIter end = *delete_end;

		gtk_text_iter_backward_lines (&start, search->text_nb_lines);
		gtk_text_iter_forward_lines (&end, search->text_nb_lines);

		remove_occurrences_in_range (search, &start, &end);
		add_subregion_to_scan (search, &start, &end);
	}
}

static void
delete_range_after_cb (GtkSourceSearchContext *search,
                       GtkTextIter            *start,
                       GtkTextIter            *end)
{
	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		update (search);
	}
	else
	{
		add_subregion_to_scan (search, start, end);
	}
}

static void
set_buffer (GtkSourceSearchContext *search,
	    GtkSourceBuffer        *buffer)
{
	g_assert (search->buffer == NULL);
	g_assert (search->tag_table == NULL);

	search->buffer = GTK_TEXT_BUFFER (buffer);

	g_object_add_weak_pointer (G_OBJECT (buffer),
				   (gpointer *)&search->buffer);

	search->tag_table = gtk_text_buffer_get_tag_table (search->buffer);
	g_object_ref (search->tag_table);

	g_signal_connect_object (buffer,
				 "insert-text",
				 G_CALLBACK (insert_text_before_cb),
				 search,
				 G_CONNECT_SWAPPED);

	g_signal_connect_object (buffer,
				 "insert-text",
				 G_CALLBACK (insert_text_after_cb),
				 search,
				 G_CONNECT_AFTER | G_CONNECT_SWAPPED);

	g_signal_connect_object (buffer,
				 "delete-range",
				 G_CALLBACK (delete_range_before_cb),
				 search,
				 G_CONNECT_SWAPPED);

	g_signal_connect_object (buffer,
				 "delete-range",
				 G_CALLBACK (delete_range_after_cb),
				 search,
				 G_CONNECT_AFTER | G_CONNECT_SWAPPED);

	search->found_tag = gtk_text_buffer_create_tag (search->buffer, NULL, NULL);
	g_object_ref (search->found_tag);

	sync_found_tag (search);

	g_signal_connect_object (search->buffer,
				 "notify::style-scheme",
				 G_CALLBACK (sync_found_tag),
				 search,
				 G_CONNECT_SWAPPED);

	_gtk_source_buffer_add_search_context (buffer, search);
}

static gint
compute_number_of_lines (const gchar *text)
{
	const gchar *p;
	gint len;
	gint nb_of_lines = 1;

	if (text == NULL)
	{
		return 0;
	}

	len = strlen (text);
	p = text;

	while (len > 0)
	{
		gint delimiter;
		gint next_paragraph;

		pango_find_paragraph_boundary (p, len, &delimiter, &next_paragraph);

		if (delimiter == next_paragraph)
		{
			/* not found */
			break;
		}

		p += next_paragraph;
		len -= next_paragraph;
		nb_of_lines++;
	}

	return nb_of_lines;
}

static void
search_text_updated (GtkSourceSearchContext *search)
{
	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		search->text_nb_lines = 0;
	}
	else
	{
		const gchar *text = gtk_source_search_settings_get_search_text (search->settings);
		search->text_nb_lines = compute_number_of_lines (text);
	}
}

static void
settings_notify_cb (GtkSourceSearchContext  *search,
                    GParamSpec              *pspec,
                    GtkSourceSearchSettings *settings)
{
	const gchar *property = g_param_spec_get_name (pspec);

	if (g_str_equal (property, "search-text"))
	{
		search_text_updated (search);
	}

	update (search);
}

static void
set_settings (GtkSourceSearchContext  *search,
              GtkSourceSearchSettings *settings)
{
	g_assert (search->settings == NULL);

	if (settings != NULL)
	{
		search->settings = g_object_ref (settings);
	}
	else
	{
		search->settings = gtk_source_search_settings_new ();
	}

	g_signal_connect_object (search->settings,
				 "notify",
				 G_CALLBACK (settings_notify_cb),
				 search,
				 G_CONNECT_SWAPPED);

	search_text_updated (search);
	update (search);

	g_object_notify_by_pspec (G_OBJECT (search),
	                          properties [PROP_SETTINGS]);
}

static void
gtk_source_search_context_dispose (GObject *object)
{
	GtkSourceSearchContext *search = GTK_SOURCE_SEARCH_CONTEXT (object);

	clear_search (search);

	if (search->found_tag != NULL &&
	    search->tag_table != NULL)
	{
		gtk_text_tag_table_remove (search->tag_table,
					   search->found_tag);

		g_clear_object (&search->found_tag);
		g_clear_object (&search->tag_table);
	}

	if (search->buffer != NULL)
	{
		g_object_remove_weak_pointer (G_OBJECT (search->buffer),
					      (gpointer *)&search->buffer);

		search->buffer = NULL;
	}

	g_clear_object (&search->settings);

	G_OBJECT_CLASS (gtk_source_search_context_parent_class)->dispose (object);
}

static void
gtk_source_search_context_finalize (GObject *object)
{
	GtkSourceSearchContext *search = GTK_SOURCE_SEARCH_CONTEXT (object);

	g_clear_pointer (&search->regex, impl_regex_unref);
	g_clear_error (&search->regex_error);

	G_OBJECT_CLASS (gtk_source_search_context_parent_class)->finalize (object);
}

static void
gtk_source_search_context_get_property (GObject    *object,
                                        guint       prop_id,
                                        GValue     *value,
                                        GParamSpec *pspec)
{
	GtkSourceSearchContext *search;

	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (object));

	search = GTK_SOURCE_SEARCH_CONTEXT (object);

	switch (prop_id)
	{
		case PROP_BUFFER:
			g_value_set_object (value, search->buffer);
			break;

		case PROP_SETTINGS:
			g_value_set_object (value, search->settings);
			break;

		case PROP_HIGHLIGHT:
			g_value_set_boolean (value, search->highlight);
			break;

		case PROP_MATCH_STYLE:
			g_value_set_object (value, search->match_style);
			break;

		case PROP_OCCURRENCES_COUNT:
			g_value_set_int (value, gtk_source_search_context_get_occurrences_count (search));
			break;

		case PROP_REGEX_ERROR:
			g_value_take_boxed (value, gtk_source_search_context_get_regex_error (search));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
			break;
	}
}

static void
gtk_source_search_context_set_property (GObject      *object,
                                        guint         prop_id,
                                        const GValue *value,
                                        GParamSpec   *pspec)
{
	GtkSourceSearchContext *search;

	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (object));

	search = GTK_SOURCE_SEARCH_CONTEXT (object);

	switch (prop_id)
	{
		case PROP_BUFFER:
			set_buffer (search, g_value_get_object (value));
			break;

		case PROP_SETTINGS:
			set_settings (search, g_value_get_object (value));
			break;

		case PROP_HIGHLIGHT:
			gtk_source_search_context_set_highlight (search, g_value_get_boolean (value));
			break;

		case PROP_MATCH_STYLE:
			gtk_source_search_context_set_match_style (search, g_value_get_object (value));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
			break;
	}
}

static void
gtk_source_search_context_class_init (GtkSourceSearchContextClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->dispose = gtk_source_search_context_dispose;
	object_class->finalize = gtk_source_search_context_finalize;
	object_class->get_property = gtk_source_search_context_get_property;
	object_class->set_property = gtk_source_search_context_set_property;

	/**
	 * GtkSourceSearchContext:buffer:
	 *
	 * The [class@Buffer] associated to the search context.
	 */
	properties [PROP_BUFFER] =
		g_param_spec_object ("buffer",
		                     "Buffer",
		                     "The associated GtkSourceBuffer",
		                     GTK_SOURCE_TYPE_BUFFER,
		                     (G_PARAM_READWRITE |
		                      G_PARAM_CONSTRUCT_ONLY |
		                      G_PARAM_STATIC_STRINGS));

	/**
	 * GtkSourceSearchContext:settings:
	 *
	 * The [class@SearchSettings] associated to the search context.
	 *
	 * This property is construct-only since version 4.0.
	 */
	properties [PROP_SETTINGS] =
		g_param_spec_object ("settings",
		                     "Settings",
		                     "The associated GtkSourceSearchSettings",
		                     GTK_SOURCE_TYPE_SEARCH_SETTINGS,
		                     (G_PARAM_READWRITE |
		                      G_PARAM_CONSTRUCT_ONLY |
		                      G_PARAM_STATIC_STRINGS));

	/**
	 * GtkSourceSearchContext:highlight:
	 *
	 * Highlight the search occurrences.
	 */
	properties [PROP_HIGHLIGHT] =
		g_param_spec_boolean ("highlight",
		                      "Highlight",
		                      "Highlight search occurrences",
		                      TRUE,
		                      (G_PARAM_READWRITE |
		                       G_PARAM_CONSTRUCT |
		                       G_PARAM_STATIC_STRINGS));

	/**
	 * GtkSourceSearchContext:match-style:
	 *
	 * A [class@Style], or %NULL for theme's scheme default style.
	 */
	properties [PROP_MATCH_STYLE] =
		g_param_spec_object ("match-style",
		                     "Match style",
		                     "The text style for matches",
		                     GTK_SOURCE_TYPE_STYLE,
		                     (G_PARAM_READWRITE |
		                      G_PARAM_CONSTRUCT |
		                      G_PARAM_STATIC_STRINGS));

	/**
	 * GtkSourceSearchContext:occurrences-count:
	 *
	 * The total number of search occurrences. If the search is disabled,
	 * the value is 0. If the buffer is not already fully scanned, the value
	 * is -1.
	 */
	properties [PROP_OCCURRENCES_COUNT] =
		g_param_spec_int ("occurrences-count",
		                  "Occurrences count",
		                  "Total number of search occurrences",
		                  -1,
		                  G_MAXINT,
		                  0,
		                  (G_PARAM_READABLE |
		                   G_PARAM_STATIC_STRINGS));

	/**
	 * GtkSourceSearchContext:regex-error:
	 *
	 * If the regex search pattern doesn't follow all the rules, this
	 * #GError property will be set. If the pattern is valid, the value is
	 * %NULL.
	 *
	 * Free with [method@GLib.Error.free].
	 */
	properties [PROP_REGEX_ERROR] =
		g_param_spec_boxed ("regex-error",
		                    "Regex error",
		                    "Regular expression error",
		                    G_TYPE_ERROR,
		                    (G_PARAM_READABLE |
		                     G_PARAM_STATIC_STRINGS));

	g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
gtk_source_search_context_init (GtkSourceSearchContext *search)
{
	search = gtk_source_search_context_get_instance_private (search);
}

/**
 * gtk_source_search_context_new:
 * @buffer: a #GtkSourceBuffer.
 * @settings: (nullable): a #GtkSourceSearchSettings, or %NULL.
 *
 * Creates a new search context, associated with @buffer, and customized with
 * @settings.
 *
 * If @settings is %NULL, a new [class@SearchSettings] object will
 * be created, that you can retrieve with [method@SearchContext.get_settings].
 *
 * Returns: a new search context.
 */
GtkSourceSearchContext *
gtk_source_search_context_new (GtkSourceBuffer         *buffer,
                               GtkSourceSearchSettings *settings)
{
	g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), NULL);
	g_return_val_if_fail (settings == NULL || GTK_SOURCE_IS_SEARCH_SETTINGS (settings), NULL);

	return g_object_new (GTK_SOURCE_TYPE_SEARCH_CONTEXT,
			     "buffer", buffer,
			     "settings", settings,
			     NULL);
}

/**
 * gtk_source_search_context_get_buffer:
 * @search: a #GtkSourceSearchContext.
 *
 * Returns: (transfer none): the associated buffer.
 */
GtkSourceBuffer *
gtk_source_search_context_get_buffer (GtkSourceSearchContext *search)
{
	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL);

	return GTK_SOURCE_BUFFER (search->buffer);
}

/**
 * gtk_source_search_context_get_settings:
 * @search: a #GtkSourceSearchContext.
 *
 * Returns: (transfer none): the search settings.
 */
GtkSourceSearchSettings *
gtk_source_search_context_get_settings (GtkSourceSearchContext *search)
{
	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL);

	return search->settings;
}

/**
 * gtk_source_search_context_get_highlight:
 * @search: a #GtkSourceSearchContext.
 *
 * Returns: whether to highlight the search occurrences.
 */
gboolean
gtk_source_search_context_get_highlight (GtkSourceSearchContext *search)
{
	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);

	return search->highlight;
}

/**
 * gtk_source_search_context_set_highlight:
 * @search: a #GtkSourceSearchContext.
 * @highlight: the setting.
 *
 * Enables or disables the search occurrences highlighting.
 */
void
gtk_source_search_context_set_highlight (GtkSourceSearchContext *search,
                                         gboolean                highlight)
{
	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));

	highlight = highlight != FALSE;

	if (search->highlight != highlight)
	{
		search->highlight = highlight;
		sync_found_tag (search);

		g_object_notify_by_pspec (G_OBJECT (search), properties [PROP_HIGHLIGHT]);
	}
}

/**
 * gtk_source_search_context_get_match_style:
 * @search: a #GtkSourceSearchContext.
 *
 * Returns: (transfer none): the #GtkSourceStyle to apply on search matches.
 */
GtkSourceStyle *
gtk_source_search_context_get_match_style (GtkSourceSearchContext *search)
{
	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL);

	return search->match_style;
}

/**
 * gtk_source_search_context_set_match_style:
 * @search: a #GtkSourceSearchContext.
 * @match_style: (nullable): a #GtkSourceStyle, or %NULL.
 *
 * Set the style to apply on search matches.
 *
 * If @match_style is %NULL, default theme's scheme 'match-style' will be used.
 * To enable or disable the search highlighting, use [method@SearchContext.set_highlight].
 */
void
gtk_source_search_context_set_match_style (GtkSourceSearchContext *search,
                                           GtkSourceStyle         *match_style)
{
	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
	g_return_if_fail (match_style == NULL || GTK_SOURCE_IS_STYLE (match_style));

	if (search->match_style == match_style)
	{
		return;
	}

	if (search->match_style != NULL)
	{
		g_object_unref (search->match_style);
	}

	search->match_style = match_style;

	if (match_style != NULL)
	{
		g_object_ref (match_style);
	}

	g_object_notify_by_pspec (G_OBJECT (search), properties [PROP_MATCH_STYLE]);
}

/**
 * gtk_source_search_context_get_regex_error:
 * @search: a #GtkSourceSearchContext.
 *
 * Regular expression patterns must follow certain rules. If
 * [property@SearchSettings:search-text] breaks a rule, the error can be
 * retrieved with this function.
 *
 * The error domain is [enum@GLib.RegexError].
 *
 * Free the return value with [method@GLib.Error.free].
 *
 * Returns: (transfer full) (nullable): the #GError, or %NULL if the
 *   pattern is valid.
 */
GError *
gtk_source_search_context_get_regex_error (GtkSourceSearchContext *search)
{
	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL);

	if (search->regex_error == NULL)
	{
		return NULL;
	}

	return g_error_copy (search->regex_error);
}

/**
 * gtk_source_search_context_get_occurrences_count:
 * @search: a #GtkSourceSearchContext.
 *
 * Gets the total number of search occurrences.
 *
 * If the buffer is not already fully scanned, the total number of occurrences is
 * unknown, and -1 is returned.
 *
 * Returns: the total number of search occurrences, or -1 if unknown.
 */
gint
gtk_source_search_context_get_occurrences_count (GtkSourceSearchContext *search)
{
	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), -1);

	if (!gtk_source_region_is_empty (search->scan_region))
	{
		return -1;
	}

	return search->occurrences_count;
}

/**
 * gtk_source_search_context_get_occurrence_position:
 * @search: a #GtkSourceSearchContext.
 * @match_start: the start of the occurrence.
 * @match_end: the end of the occurrence.
 *
 * Gets the position of a search occurrence.
 *
 * If the buffer is not already fully scanned, the position may be unknown,
 * and -1 is returned. If 0 is returned, it means that this part of the buffer
 * has already been scanned, and that @match_start and @match_end don't delimit an occurrence.
 *
 * Returns: the position of the search occurrence. The first occurrence has the
 * position 1 (not 0). Returns 0 if @match_start and @match_end don't delimit
 * an occurrence. Returns -1 if the position is not yet known.
 */
gint
gtk_source_search_context_get_occurrence_position (GtkSourceSearchContext *search,
                                                   const GtkTextIter      *match_start,
                                                   const GtkTextIter      *match_end)
{
	GtkTextIter m_start;
	GtkTextIter m_end;
	GtkTextIter iter;
	gboolean found;
	gint position = 0;
	GtkSourceRegion *region;
	gboolean empty;

	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), -1);
	g_return_val_if_fail (match_start != NULL, -1);
	g_return_val_if_fail (match_end != NULL, -1);

	if (search->buffer == NULL)
	{
		return -1;
	}

	/* Verify that the [match_start; match_end] region has been scanned. */

	if (search->scan_region != NULL)
	{
		region = gtk_source_region_intersect_subregion (search->scan_region,
								match_start,
								match_end);

		empty = gtk_source_region_is_empty (region);

		g_clear_object (&region);

		if (!empty)
		{
			return -1;
		}
	}

	/* Verify that the occurrence is correct. */

	found = smart_forward_search_without_scanning (search,
						       match_start,
						       &m_start,
						       &m_end,
						       match_end);

	if (!found ||
	    !gtk_text_iter_equal (match_start, &m_start) ||
	    !gtk_text_iter_equal (match_end, &m_end))
	{
		return 0;
	}

	/* Verify that the scan region is empty between the start of the buffer
	 * and the end of the occurrence.
	 */

	gtk_text_buffer_get_start_iter (search->buffer, &iter);

	if (search->scan_region != NULL)
	{
		region = gtk_source_region_intersect_subregion (search->scan_region,
								&iter,
								match_end);

		empty = gtk_source_region_is_empty (region);

		g_clear_object (&region);

		if (!empty)
		{
			return -1;
		}
	}

	/* Everything is fine, count the number of previous occurrences. */

	while (smart_forward_search_without_scanning (search, &iter, &m_start, &m_end, match_start))
	{
		position++;
		iter = m_end;
	}

	return position + 1;
}

/**
 * gtk_source_search_context_forward:
 * @search: a #GtkSourceSearchContext.
 * @iter: start of search.
 * @match_start: (out) (optional): return location for start of match, or %NULL.
 * @match_end: (out) (optional): return location for end of match, or %NULL.
 * @has_wrapped_around: (out) (optional): return location to know whether the
 *   search has wrapped around, or %NULL.
 *
 * Synchronous forward search.
 *
 * It is recommended to use the asynchronous functions instead, to not block the user interface.
 * However, if you are sure that the @buffer is small, this function is more convenient to use.
 *
 * If the [property@SearchSettings:wrap-around] property is %FALSE, this function
 * doesn't try to wrap around.
 *
 * The @has_wrapped_around out parameter is set independently of whether a match
 * is found. So if this function returns %FALSE, @has_wrapped_around will have
 * the same value as the  [property@SearchSettings:wrap-around] property.
 *
 * Returns: whether a match was found.
 */
gboolean
gtk_source_search_context_forward (GtkSourceSearchContext *search,
                                   const GtkTextIter      *iter,
                                   GtkTextIter            *match_start,
                                   GtkTextIter            *match_end,
                                   gboolean               *has_wrapped_around)
{
	GtkTextIter m_start;
	GtkTextIter m_end;
	gboolean found;

	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);
	g_return_val_if_fail (iter != NULL, FALSE);

	if (has_wrapped_around != NULL)
	{
		*has_wrapped_around = FALSE;
	}

	if (search->buffer == NULL)
	{
		return FALSE;
	}

	found = smart_forward_search (search, iter, &m_start, &m_end);

	if (!found && gtk_source_search_settings_get_wrap_around (search->settings))
	{
		GtkTextIter start_iter;
		gtk_text_buffer_get_start_iter (search->buffer, &start_iter);

		found = smart_forward_search (search, &start_iter, &m_start, &m_end);

		if (has_wrapped_around != NULL)
		{
			*has_wrapped_around = TRUE;
		}
	}

	if (found && match_start != NULL)
	{
		*match_start = m_start;
	}

	if (found && match_end != NULL)
	{
		*match_end = m_end;
	}

	return found;
}

/**
 * gtk_source_search_context_forward_async:
 * @search: a #GtkSourceSearchContext.
 * @iter: start of search.
 * @cancellable: (nullable): a #GCancellable, or %NULL.
 * @callback: a #GAsyncReadyCallback to call when the operation is finished.
 * @user_data: the data to pass to the @callback function.
 *
 * The asynchronous version of [method@SearchContext.forward].
 *
 * See the [iface@Gio.AsyncResult] documentation to know how to use this function.
 *
 * If the operation is cancelled, the @callback will only be called if
 * @cancellable was not %NULL. The method takes
 * ownership of @cancellable, so you can unref it after calling this function.
 */
void
gtk_source_search_context_forward_async (GtkSourceSearchContext *search,
                                         const GtkTextIter      *iter,
                                         GCancellable           *cancellable,
                                         GAsyncReadyCallback     callback,
                                         gpointer                user_data)
{
	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
	g_return_if_fail (iter != NULL);

	if (search->buffer == NULL)
	{
		return;
	}

	clear_task (search);
	search->task = g_task_new (search, cancellable, callback, user_data);

	smart_forward_search_async (search, iter, FALSE);
}

/**
 * gtk_source_search_context_forward_finish:
 * @search: a #GtkSourceSearchContext.
 * @result: a #GAsyncResult.
 * @match_start: (out) (optional): return location for start of match, or %NULL.
 * @match_end: (out) (optional): return location for end of match, or %NULL.
 * @has_wrapped_around: (out) (optional): return location to know whether the
 *   search has wrapped around, or %NULL.
 * @error: a #GError, or %NULL.
 *
 * Finishes a forward search started with [method@SearchContext.forward_async].
 *
 * See the documentation of [method@SearchContext.forward] for more
 * details.
 *
 * Returns: whether a match was found.
 */
gboolean
gtk_source_search_context_forward_finish (GtkSourceSearchContext  *search,
                                          GAsyncResult            *result,
                                          GtkTextIter             *match_start,
                                          GtkTextIter             *match_end,
                                          gboolean                *has_wrapped_around,
                                          GError                 **error)
{
	ForwardBackwardData *data;
	gboolean found;

	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);

	if (has_wrapped_around != NULL)
	{
		*has_wrapped_around = FALSE;
	}

	if (search->buffer == NULL)
	{
		return FALSE;
	}

	g_return_val_if_fail (g_task_is_valid (result, search), FALSE);

	data = g_task_propagate_pointer (G_TASK (result), error);

	if (data == NULL)
	{
		return FALSE;
	}

	found = data->found;

	if (found)
	{
		if (match_start != NULL)
		{
                        gtk_text_buffer_get_iter_at_mark (search->buffer,
                                                          match_start,
                                                          data->match_start);
		}

		if (match_end != NULL)
		{
                        gtk_text_buffer_get_iter_at_mark (search->buffer,
                                                          match_end,
                                                          data->match_end);
		}
	}

	if (has_wrapped_around != NULL)
	{
		*has_wrapped_around = data->wrapped_around;
	}

	forward_backward_data_free (data);
	return found;
}

/**
 * gtk_source_search_context_backward:
 * @search: a #GtkSourceSearchContext.
 * @iter: start of search.
 * @match_start: (out) (optional): return location for start of match, or %NULL.
 * @match_end: (out) (optional): return location for end of match, or %NULL.
 * @has_wrapped_around: (out) (optional): return location to know whether the
 *   search has wrapped around, or %NULL.
 *
 * Synchronous backward search.
 *
 * It is recommended to use the asynchronous functions instead, to not block the user interface.
 * However, if you are sure that the @buffer is small, this function is more convenient to use.
 *
 * If the [property@SearchSettings:wrap-around] property is %FALSE, this function
 * doesn't try to wrap around.
 *
 * The @has_wrapped_around out parameter is set independently of whether a match
 * is found. So if this function returns %FALSE, @has_wrapped_around will have
 * the same value as the [property@SearchSettings:wrap-around] property.
 *
 * Returns: whether a match was found.
 */
gboolean
gtk_source_search_context_backward (GtkSourceSearchContext *search,
                                    const GtkTextIter      *iter,
                                    GtkTextIter            *match_start,
                                    GtkTextIter            *match_end,
                                    gboolean               *has_wrapped_around)
{
	GtkTextIter m_start;
	GtkTextIter m_end;
	gboolean found;

	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);
	g_return_val_if_fail (iter != NULL, FALSE);

	if (has_wrapped_around != NULL)
	{
		*has_wrapped_around = FALSE;
	}

	if (search->buffer == NULL)
	{
		return FALSE;
	}

	found = smart_backward_search (search, iter, &m_start, &m_end);

	if (!found && gtk_source_search_settings_get_wrap_around (search->settings))
	{
		GtkTextIter end_iter;

		gtk_text_buffer_get_end_iter (search->buffer, &end_iter);

		found = smart_backward_search (search, &end_iter, &m_start, &m_end);

		if (has_wrapped_around != NULL)
		{
			*has_wrapped_around = TRUE;
		}
	}

	if (found && match_start != NULL)
	{
		*match_start = m_start;
	}

	if (found && match_end != NULL)
	{
		*match_end = m_end;
	}

	return found;
}

/**
 * gtk_source_search_context_backward_async:
 * @search: a #GtkSourceSearchContext.
 * @iter: start of search.
 * @cancellable: (nullable): a #GCancellable, or %NULL.
 * @callback: a #GAsyncReadyCallback to call when the operation is finished.
 * @user_data: the data to pass to the @callback function.
 *
 * The asynchronous version of [method@SearchContext.backward].
 *
 * See the [iface@Gio.AsyncResult] documentation to know how to use this function.
 *
 * If the operation is cancelled, the @callback will only be called if
 * @cancellable was not %NULL. The method takes
 * ownership of @cancellable, so you can unref it after calling this function.
 */
void
gtk_source_search_context_backward_async (GtkSourceSearchContext *search,
                                          const GtkTextIter      *iter,
                                          GCancellable           *cancellable,
                                          GAsyncReadyCallback     callback,
                                          gpointer                user_data)
{
	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
	g_return_if_fail (iter != NULL);

	if (search->buffer == NULL)
	{
		return;
	}

	clear_task (search);
	search->task = g_task_new (search, cancellable, callback, user_data);

	smart_backward_search_async (search, iter, FALSE);
}

/**
 * gtk_source_search_context_backward_finish:
 * @search: a #GtkSourceSearchContext.
 * @result: a #GAsyncResult.
 * @match_start: (out) (optional): return location for start of match, or %NULL.
 * @match_end: (out) (optional): return location for end of match, or %NULL.
 * @has_wrapped_around: (out) (optional): return location to know whether the
 *   search has wrapped around, or %NULL.
 * @error: a #GError, or %NULL.
 *
 * Finishes a backward search started with
 * [method@SearchContext.backward_async].
 *
 * See the documentation of [method@SearchContext.backward] for more
 * details.
 *
 * Returns: whether a match was found.
 */
gboolean
gtk_source_search_context_backward_finish (GtkSourceSearchContext  *search,
                                           GAsyncResult            *result,
                                           GtkTextIter             *match_start,
                                           GtkTextIter             *match_end,
                                           gboolean                *has_wrapped_around,
                                           GError                 **error)
{
	return gtk_source_search_context_forward_finish (search,
							 result,
							 match_start,
							 match_end,
							 has_wrapped_around,
							 error);
}

/* If correctly replaced, returns %TRUE and @match_end is updated to point to
 * the replacement end.
 */
static gboolean
regex_replace (GtkSourceSearchContext  *search,
	       const GtkTextIter       *match_start,
	       GtkTextIter             *match_end,
	       const gchar             *replace,
	       GError                 **error)
{
	GtkTextIter real_start;
	GtkTextIter real_end;
	GtkTextIter match_start_check;
	GtkTextIter match_end_check;
	GtkTextIter match_start_copy;
	gint start_pos;
	gchar *subject;
	gchar *suffix;
	gchar *subject_replaced;
	GRegexMatchFlags match_options;
	GError *tmp_error = NULL;
	gboolean replaced = FALSE;

	if (search->regex == NULL ||
	    search->regex_error != NULL)
	{
		return FALSE;
	}

	regex_search_get_real_start (search, match_start, &real_start, &start_pos);
	g_assert_cmpint (start_pos, >=, 0);

	if (!basic_forward_regex_search (search,
					 match_start,
					 &match_start_check,
					 &match_end_check,
					 &real_end,
					 match_end))
	{
		g_assert_not_reached ();
	}

	g_assert (gtk_text_iter_equal (match_start, &match_start_check));
	g_assert (gtk_text_iter_equal (match_end, &match_end_check));

	subject = gtk_text_iter_get_visible_text (&real_start, &real_end);

	suffix = gtk_text_iter_get_visible_text (match_end, &real_end);
	if (suffix == NULL)
	{
		suffix = g_strdup ("");
	}

	match_options = regex_search_get_match_options (&real_start, &real_end);
	match_options |= G_REGEX_MATCH_ANCHORED;

	subject_replaced = impl_regex_replace (search->regex,
	                                       subject,
	                                       -1,
	                                       start_pos,
	                                       replace,
	                                       match_options,
	                                       &tmp_error);

	if (tmp_error != NULL)
	{
		g_propagate_error (error, tmp_error);
		goto end;
	}

	g_return_val_if_fail (g_str_has_suffix (subject_replaced, suffix), FALSE);

	/* Truncate subject_replaced to not contain the suffix, so we can
	 * replace only [match_start, match_end], not [match_start, real_end].
	 * The first solution is slightly simpler, and avoids the need to
	 * re-scan [match_end, real_end] for matches, which is convenient for a
	 * replace all.
	 */
	subject_replaced[strlen (subject_replaced) - strlen (suffix)] = '\0';
	g_return_val_if_fail (strlen (subject_replaced) >= (guint)start_pos, FALSE);

	match_start_copy = *match_start;

	gtk_text_buffer_begin_user_action (search->buffer);
	gtk_text_buffer_delete (search->buffer, &match_start_copy, match_end);
	gtk_text_buffer_insert (search->buffer, match_end, subject_replaced + start_pos, -1);
	gtk_text_buffer_end_user_action (search->buffer);

	replaced = TRUE;

end:
	g_free (subject);
	g_free (suffix);
	g_free (subject_replaced);
	return replaced;
}

/**
 * gtk_source_search_context_replace:
 * @search: a #GtkSourceSearchContext.
 * @match_start: the start of the match to replace.
 * @match_end: the end of the match to replace.
 * @replace: the replacement text.
 * @replace_length: the length of @replace in bytes, or -1.
 * @error: location to a #GError, or %NULL to ignore errors.
 *
 * Replaces a search match by another text. If @match_start and @match_end
 * doesn't correspond to a search match, %FALSE is returned.
 *
 * @match_start and @match_end iters are revalidated to point to the replacement
 * text boundaries.
 *
 * For a regular expression replacement, you can check if @replace is valid by
 * calling [func@GLib.Regex.check_replacement]. The @replace text can contain
 * backreferences.
 *
 * Returns: whether the match has been replaced.
 */
gboolean
gtk_source_search_context_replace (GtkSourceSearchContext  *search,
                                   GtkTextIter             *match_start,
                                   GtkTextIter             *match_end,
                                   const gchar             *replace,
                                   gint                     replace_length,
                                   GError                 **error)
{
	GtkTextIter start;
	GtkTextIter end;
	GtkTextMark *start_mark;
	gboolean replaced = FALSE;

	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE);
	g_return_val_if_fail (match_start != NULL, FALSE);
	g_return_val_if_fail (match_end != NULL, FALSE);
	g_return_val_if_fail (replace != NULL, FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	if (search->buffer == NULL)
	{
		return FALSE;
	}

	if (!smart_forward_search (search, match_start, &start, &end))
	{
		return FALSE;
	}

	if (!gtk_text_iter_equal (match_start, &start) ||
	    !gtk_text_iter_equal (match_end, &end))
	{
		return FALSE;
	}

	start_mark = gtk_text_buffer_create_mark (search->buffer, NULL, &start, TRUE);

	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		replaced = regex_replace (search, &start, &end, replace, error);
	}
	else
	{
		gtk_text_buffer_begin_user_action (search->buffer);
		gtk_text_buffer_delete (search->buffer, &start, &end);
		gtk_text_buffer_insert (search->buffer, &end, replace, replace_length);
		gtk_text_buffer_end_user_action (search->buffer);

		replaced = TRUE;
	}

	if (replaced)
	{
		gtk_text_buffer_get_iter_at_mark (search->buffer, match_start, start_mark);
		*match_end = end;
	}

	gtk_text_buffer_delete_mark (search->buffer, start_mark);

	return replaced;
}

/**
 * gtk_source_search_context_replace_all:
 * @search: a #GtkSourceSearchContext.
 * @replace: the replacement text.
 * @replace_length: the length of @replace in bytes, or -1.
 * @error: location to a #GError, or %NULL to ignore errors.
 *
 * Replaces all search matches by another text.
 *
 * It is a synchronous function, so it can block the user interface.
 *
 * For a regular expression replacement, you can check if @replace is valid by
 * calling [func@GLib.Regex.check_replacement]. The @replace text can contain
 * backreferences.
 *
 * Returns: the number of replaced matches.
 */
guint
gtk_source_search_context_replace_all (GtkSourceSearchContext  *search,
                                       const gchar             *replace,
                                       gint                     replace_length,
                                       GError                 **error)
{
	GtkTextIter iter;
	GtkTextIter match_start;
	GtkTextIter match_end;
	guint nb_matches_replaced = 0;
	gboolean highlight_matching_brackets;
	gboolean has_regex_references = FALSE;

	g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), 0);
	g_return_val_if_fail (replace != NULL, 0);
	g_return_val_if_fail (error == NULL || *error == NULL, 0);

	if (search->buffer == NULL)
	{
		return 0;
	}

	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		GError *tmp_error = NULL;

		if (search->regex == NULL ||
		    search->regex_error != NULL)
		{
			return 0;
		}

		g_regex_check_replacement (replace,
					   &has_regex_references,
					   &tmp_error);

		if (tmp_error != NULL)
		{
			g_propagate_error (error, tmp_error);
			return 0;
		}
	}

	g_signal_handlers_block_by_func (search->buffer, insert_text_before_cb, search);
	g_signal_handlers_block_by_func (search->buffer, insert_text_after_cb, search);
	g_signal_handlers_block_by_func (search->buffer, delete_range_before_cb, search);
	g_signal_handlers_block_by_func (search->buffer, delete_range_after_cb, search);

	highlight_matching_brackets =
		gtk_source_buffer_get_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->buffer));

	gtk_source_buffer_set_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->buffer),
							   FALSE);

	_gtk_source_buffer_save_and_clear_selection (GTK_SOURCE_BUFFER (search->buffer));

	gtk_text_buffer_get_start_iter (search->buffer, &iter);

	gtk_text_buffer_begin_user_action (search->buffer);

	while (smart_forward_search (search, &iter, &match_start, &match_end))
	{
		if (has_regex_references)
		{
			if (!regex_replace (search, &match_start, &match_end, replace, error))
			{
				break;
			}
		}
		else
		{
			gtk_text_buffer_delete (search->buffer, &match_start, &match_end);
			gtk_text_buffer_insert (search->buffer, &match_end, replace, replace_length);
		}

		nb_matches_replaced++;
		iter = match_end;
	}

	gtk_text_buffer_end_user_action (search->buffer);

	_gtk_source_buffer_restore_selection (GTK_SOURCE_BUFFER (search->buffer));

	gtk_source_buffer_set_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->buffer),
							   highlight_matching_brackets);

	g_signal_handlers_unblock_by_func (search->buffer, insert_text_before_cb, search);
	g_signal_handlers_unblock_by_func (search->buffer, insert_text_after_cb, search);
	g_signal_handlers_unblock_by_func (search->buffer, delete_range_before_cb, search);
	g_signal_handlers_unblock_by_func (search->buffer, delete_range_after_cb, search);

	update (search);

	return nb_matches_replaced;
}

/* Highlight the [start,end] region in priority. */
void
_gtk_source_search_context_update_highlight (GtkSourceSearchContext *search,
                                             const GtkTextIter      *start,
                                             const GtkTextIter      *end,
                                             gboolean                synchronous)
{
	GtkSourceRegion *region_to_highlight = NULL;

	g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search));
	g_return_if_fail (start != NULL);
	g_return_if_fail (end != NULL);

	if (search->buffer == NULL ||
	    gtk_source_region_is_empty (search->scan_region) ||
	    !search->highlight)
	{
		return;
	}

	region_to_highlight = gtk_source_region_intersect_subregion (search->scan_region,
								     start,
								     end);

	if (gtk_source_region_is_empty (region_to_highlight))
	{
		goto out;
	}

	if (!synchronous)
	{
		if (search->high_priority_region == NULL)
		{
			search->high_priority_region = region_to_highlight;
			region_to_highlight = NULL;
		}
		else
		{
			gtk_source_region_add_region (search->high_priority_region,
						      region_to_highlight);
		}

		install_idle_scan (search);
		goto out;
	}

	if (gtk_source_search_settings_get_regex_enabled (search->settings))
	{
		GtkTextIter region_start;

		if (!gtk_source_region_get_bounds (search->scan_region,
						   &region_start,
						   NULL))
		{
			goto out;
		}

		regex_search_scan_chunk (search, &region_start, end);
	}
	else
	{
		scan_all_region (search, region_to_highlight);
	}

out:
	g_clear_object (&region_to_highlight);
}
