/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */ /* gtksourcesearchcontext.c * This file is part of GtkSourceView * * Copyright (C) 2013-2016 - SĂ©bastien Wilmet * * 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, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #ifdef HAVE_CONFIG_H #include #endif #include "gtksourcesearchcontext.h" #include "gtksourcesearchsettings.h" #include "gtksourcebuffer.h" #include "gtksourcebuffer-private.h" #include "gtksourcebufferinternal.h" #include "gtksourcestyle.h" #include "gtksourcestylescheme.h" #include "gtksourceutils.h" #include "gtksourceregion.h" #include "gtksourceiter.h" #include "gtksourceview-i18n.h" #include "gtksourceview-enumtypes.h" #include /** * SECTION:searchcontext * @Short_description: Search context * @Title: GtkSourceSearchContext * @See_also: #GtkSourceBuffer, #GtkSourceSearchSettings * * A #GtkSourceSearchContext is used for the search and replace in a * #GtkSourceBuffer. The search settings are represented by a * #GtkSourceSearchSettings 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 * gtk_source_search_context_get_occurrences_count(). To know the position of a * certain match, use gtk_source_search_context_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 gtk_source_search_context_forward() or * gtk_source_search_context_forward_async() for the asynchronous version. * The backward search is done similarly. To replace a search match, or all * matches, use gtk_source_search_context_replace() and * gtk_source_search_context_replace_all(). * * The search occurrences are highlighted by default. To disable it, use * gtk_source_search_context_set_highlight(). You can enable the search * highlighting for several #GtkSourceSearchContexts attached to the * same buffer. Moreover, each of those #GtkSourceSearchContexts can * have a different text style associated. Use * gtk_source_search_context_set_match_style() to specify the #GtkSourceStyle * to apply on search matches. * * Note that the #GtkSourceSearchContext:highlight and * #GtkSourceSearchContext:match-style properties are in the * #GtkSourceSearchContext class, not #GtkSourceSearchSettings. Appearance * settings should be tied to one, and only one buffer, as different buffers can * have different style scheme associated (a #GtkSourceSearchSettings 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: * [1 GB of text] * 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 }; struct _GtkSourceSearchContextPrivate { /* 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; GRegex *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; GtkTextIter match_start; GtkTextIter match_end; guint found : 1; guint wrapped_around : 1; /* forward or backward */ guint is_forward : 1; } ForwardBackwardData; G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceSearchContext, gtk_source_search_context, G_TYPE_OBJECT); 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->priv->match_style; GtkSourceStyleScheme *style_scheme; if (search->priv->buffer == NULL) { return; } if (!search->priv->highlight) { gtk_source_style_apply (NULL, search->priv->found_tag); return; } if (style == NULL) { style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (search->priv->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->priv->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, ®ion_iter); while (!gtk_source_region_iter_is_end (®ion_iter)) { if (!gtk_source_region_iter_get_subregion (®ion_iter, start, end)) { return FALSE; } if (!gtk_text_iter_equal (start, end)) { return TRUE; } gtk_source_region_iter_next (®ion_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, ®ion_iter); while (!gtk_source_region_iter_is_end (®ion_iter)) { GtkTextIter start_subregion; GtkTextIter end_subregion; if (!gtk_source_region_iter_get_subregion (®ion_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 (®ion_iter); } return found; } static void clear_task (GtkSourceSearchContext *search) { g_clear_object (&search->priv->task_region); if (search->priv->task != NULL) { GCancellable *cancellable = g_task_get_cancellable (search->priv->task); if (cancellable != NULL) { g_cancellable_cancel (cancellable); g_task_return_error_if_cancelled (search->priv->task); } g_clear_object (&search->priv->task); } } static void clear_search (GtkSourceSearchContext *search) { g_clear_object (&search->priv->scan_region); g_clear_object (&search->priv->high_priority_region); if (search->priv->idle_scan_id != 0) { g_source_remove (search->priv->idle_scan_id); search->priv->idle_scan_id = 0; } if (search->priv->regex_error != NULL) { g_clear_error (&search->priv->regex_error); g_object_notify (G_OBJECT (search), "regex-error"); } clear_task (search); search->priv->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->priv->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 = g_regex_get_max_lookbehind (search->priv->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. * g_match_info_fetch_pos() returns byte positions. To get the iters, we need to * know the number of UTF-8 characters. A GMatchInfo can contain several matches * (with g_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 (GMatchInfo *match_info, const gchar *subject, gssize subject_length, GtkTextIter *iter, gint *iter_byte_pos, GtkTextIter *match_start, GtkTextIter *match_end) { gint start_byte_pos; gint end_byte_pos; gint nb_chars; g_assert (*iter_byte_pos <= subject_length); g_assert (match_start != NULL); g_assert (match_end != NULL); if (!g_match_info_matches (match_info)) { return FALSE; } if (!g_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->priv->regex == NULL || search->priv->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->priv->buffer, &end); } else { end = *limit; } while (TRUE) { GRegexMatchFlags match_options; gchar *subject; gssize subject_length; GMatchInfo *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); g_regex_match_full (search->priv->regex, subject, subject_length, start_pos, match_options, &match_info, &search->priv->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 && g_match_info_is_partial_match (match_info)) { gtk_text_iter_forward_lines (&end, nb_lines); nb_lines <<= 1; g_free (subject); g_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->priv->regex_error != NULL) { g_object_notify (G_OBJECT (search), "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); g_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->priv->settings); GtkTextSearchFlags flags; if (search_text == NULL) { return FALSE; } if (gtk_source_search_settings_get_regex_enabled (search->priv->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->priv->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->priv->regex == NULL || search->priv->regex_error != NULL) { return FALSE; } if (limit == NULL) { gtk_text_buffer_get_start_iter (search->priv->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->priv->settings); GtkTextSearchFlags flags; if (search_text == NULL) { return FALSE; } if (gtk_source_search_settings_get_regex_enabled (search->priv->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->priv->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); } 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->priv->settings); if (gtk_text_iter_is_end (start_at)) { if (search_text != NULL && !*wrapped_around && gtk_source_search_settings_get_wrap_around (search->priv->settings)) { gtk_text_buffer_get_start_iter (search->priv->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->priv->task, task_data, (GDestroyNotify)forward_backward_data_free); g_clear_object (&search->priv->task); return TRUE; } if (!gtk_text_iter_has_tag (&iter, search->priv->found_tag)) { gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag); } else if (!gtk_text_iter_starts_tag (&iter, search->priv->found_tag)) { gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag); region_start = iter; } limit = iter; gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->found_tag); if (search->priv->scan_region != NULL) { region = gtk_source_region_intersect_subregion (search->priv->scan_region, ®ion_start, &limit); } if (gtk_source_region_is_empty (region)) { GtkTextIter match_start; GtkTextIter match_end; g_clear_object (®ion); 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 = match_start; task_data->match_end = match_end; task_data->is_forward = TRUE; task_data->wrapped_around = *wrapped_around; g_task_return_pointer (search->priv->task, task_data, (GDestroyNotify)forward_backward_data_free); g_clear_object (&search->priv->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->priv->buffer, NULL, start_at, TRUE); g_task_set_task_data (search->priv->task, task_data, (GDestroyNotify)forward_backward_data_free); g_clear_object (&search->priv->task_region); search->priv->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->priv->settings); if (gtk_text_iter_is_start (start_at)) { if (search_text != NULL && !*wrapped_around && gtk_source_search_settings_get_wrap_around (search->priv->settings)) { gtk_text_buffer_get_end_iter (search->priv->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->priv->task, task_data, (GDestroyNotify)forward_backward_data_free); g_clear_object (&search->priv->task); return TRUE; } if (gtk_text_iter_starts_tag (&iter, search->priv->found_tag) || (!gtk_text_iter_has_tag (&iter, search->priv->found_tag) && !gtk_text_iter_ends_tag (&iter, search->priv->found_tag))) { gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag); } else if (gtk_text_iter_has_tag (&iter, search->priv->found_tag)) { gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag); region_end = iter; } limit = iter; gtk_text_iter_backward_to_tag_toggle (&limit, search->priv->found_tag); if (search->priv->scan_region != NULL) { region = gtk_source_region_intersect_subregion (search->priv->scan_region, &limit, ®ion_end); } if (gtk_source_region_is_empty (region)) { GtkTextIter match_start; GtkTextIter match_end; g_clear_object (®ion); 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 = match_start; task_data->match_end = match_end; task_data->is_forward = FALSE; task_data->wrapped_around = *wrapped_around; g_task_return_pointer (search->priv->task, task_data, (GDestroyNotify)forward_backward_data_free); g_clear_object (&search->priv->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->priv->buffer, NULL, start_at, TRUE); g_task_set_task_data (search->priv->task, task_data, (GDestroyNotify)forward_backward_data_free); g_clear_object (&search->priv->task_region); search->priv->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->priv->text_nb_lines - 1)); gtk_text_iter_forward_lines (end, MAX (0, search->priv->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->priv->found_tag)) { if (gtk_source_region_is_empty (search->priv->scan_region)) { /* 'start' is in a correct match, we can skip it. */ gtk_text_iter_forward_to_tag_toggle (start, search->priv->found_tag); } else { GtkTextIter tag_start = *start; GtkTextIter tag_end = *start; GtkSourceRegion *region; if (!gtk_text_iter_starts_tag (&tag_start, search->priv->found_tag)) { gtk_text_iter_backward_to_tag_toggle (&tag_start, search->priv->found_tag); } gtk_text_iter_forward_to_tag_toggle (&tag_end, search->priv->found_tag); region = gtk_source_region_intersect_subregion (search->priv->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 (®ion); } } /* Symmetric for 'end'. */ if (gtk_text_iter_has_tag (end, search->priv->found_tag)) { if (gtk_source_region_is_empty (search->priv->scan_region)) { /* 'end' is in a correct match, we can skip it. */ if (!gtk_text_iter_starts_tag (end, search->priv->found_tag)) { gtk_text_iter_backward_to_tag_toggle (end, search->priv->found_tag); } } else { GtkTextIter tag_start = *end; GtkTextIter tag_end = *end; GtkSourceRegion *region; if (!gtk_text_iter_starts_tag (&tag_start, search->priv->found_tag)) { gtk_text_iter_backward_to_tag_toggle (&tag_start, search->priv->found_tag); } gtk_text_iter_forward_to_tag_toggle (&tag_end, search->priv->found_tag); region = gtk_source_region_intersect_subregion (search->priv->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 (®ion); } } 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->priv->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->priv->found_tag)) { gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag); } else if (!gtk_text_iter_starts_tag (&iter, search->priv->found_tag)) { gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag); } limit = iter; gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->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->priv->found_tag) && !gtk_text_iter_starts_tag (start, search->priv->found_tag)) || (gtk_source_search_settings_get_at_word_boundaries (search->priv->settings) && gtk_text_iter_ends_tag (start, search->priv->found_tag))) { gtk_text_iter_backward_to_tag_toggle (start, search->priv->found_tag); } if ((gtk_text_iter_has_tag (end, search->priv->found_tag) && !gtk_text_iter_starts_tag (end, search->priv->found_tag)) || (gtk_source_search_settings_get_at_word_boundaries (search->priv->settings) && gtk_text_iter_starts_tag (end, search->priv->found_tag))) { gtk_text_iter_forward_to_tag_toggle (end, search->priv->found_tag); } iter = *start; while (smart_forward_search_without_scanning (search, &iter, &match_start, &match_end, end)) { if (search->priv->scan_region == NULL) { /* The occurrence has already been scanned, and thus * occurrence_count take it into account. */ search->priv->occurrences_count--; } else { GtkSourceRegion *region; region = gtk_source_region_intersect_subregion (search->priv->scan_region, &match_start, &match_end); if (gtk_source_region_is_empty (region)) { search->priv->occurrences_count--; } g_clear_object (®ion); } iter = match_end; } gtk_text_buffer_remove_tag (search->priv->buffer, search->priv->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->priv->settings); /* Make sure the 'found' tag has the priority over syntax highlighting * tags. */ text_tag_set_highest_priority (search->priv->found_tag, search->priv->buffer); adjust_subregion (search, start, end); remove_occurrences_in_range (search, start, end); if (search->priv->scan_region != NULL) { DEBUG ({ g_print ("Region to scan, before:\n"); print_region (search->priv->scan_region); }); gtk_source_region_subtract_subregion (search->priv->scan_region, start, end); DEBUG ({ g_print ("Region to scan, after:\n"); print_region (search->priv->scan_region); }); } if (search->priv->task_region != NULL) { gtk_source_region_subtract_subregion (search->priv->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->priv->buffer, search->priv->found_tag, &match_start, &match_end); search->priv->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, ®ion_iter); while (!gtk_source_region_iter_is_end (®ion_iter)) { GtkTextIter subregion_start; GtkTextIter subregion_end; if (!gtk_source_region_iter_get_subregion (®ion_iter, &subregion_start, &subregion_end)) { break; } scan_subregion (search, &subregion_start, &subregion_end); gtk_source_region_iter_next (®ion_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->priv->task); GtkTextIter start_at; g_clear_object (&search->priv->task_region); gtk_text_buffer_get_iter_at_mark (search->priv->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->priv->task); if (task_data->is_forward) { scan_region_forward (search, search->priv->task_region); } else { scan_region_backward (search, search->priv->task_region); } resume_task (search); } static gboolean idle_scan_normal_search (GtkSourceSearchContext *search) { if (search->priv->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->priv->high_priority_region); g_clear_object (&search->priv->high_priority_region); return G_SOURCE_CONTINUE; } if (search->priv->task_region != NULL) { scan_task_region (search); return G_SOURCE_CONTINUE; } scan_region_forward (search, search->priv->scan_region); if (gtk_source_region_is_empty (search->priv->scan_region)) { search->priv->idle_scan_id = 0; g_object_notify (G_OBJECT (search), "occurrences-count"); g_clear_object (&search->priv->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->priv->high_priority_region, search->priv->scan_region); if (region == NULL) { return; } gtk_source_region_get_start_region_iter (region, ®ion_iter); while (!gtk_source_region_iter_is_end (®ion_iter)) { GtkTextIter subregion_start; GtkTextIter subregion_end; if (!gtk_source_region_iter_get_subregion (®ion_iter, &subregion_start, &subregion_end)) { break; } gtk_text_buffer_remove_tag (search->priv->buffer, search->priv->found_tag, &subregion_start, &subregion_end); gtk_source_region_iter_next (®ion_iter); } g_clear_object (®ion); } /* 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; GMatchInfo *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->priv->buffer, search->priv->found_tag, segment_start, segment_end); if (search->priv->regex == NULL || search->priv->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); }); g_regex_match_full (search->priv->regex, subject, subject_length, start_pos, match_options, &match_info, &search->priv->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->priv->buffer, search->priv->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->priv->occurrences_count++; g_match_info_next (match_info, &search->priv->regex_error); } if (search->priv->regex_error != NULL) { g_object_notify (G_OBJECT (search), "regex-error"); } if (g_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); g_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->priv->scan_region, chunk_start, &segment_start); if (search->priv->task_region != NULL) { gtk_source_region_subtract_subregion (search->priv->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->priv->scan_region)) { return; } if (!gtk_source_region_get_bounds (search->priv->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->priv->high_priority_region != NULL) { regex_search_handle_high_priority_region (search); g_clear_object (&search->priv->high_priority_region); return G_SOURCE_CONTINUE; } regex_search_scan_next_chunk (search); if (search->priv->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->priv->scan_region)) { search->priv->idle_scan_id = 0; g_object_notify (G_OBJECT (search), "occurrences-count"); g_clear_object (&search->priv->scan_region); return G_SOURCE_REMOVE; } return G_SOURCE_CONTINUE; } static gboolean idle_scan_cb (GtkSourceSearchContext *search) { if (search->priv->buffer == NULL) { search->priv->idle_scan_id = 0; clear_search (search); return G_SOURCE_REMOVE; } return gtk_source_search_settings_get_regex_enabled (search->priv->settings) ? idle_scan_regex_search (search) : idle_scan_normal_search (search); } static void install_idle_scan (GtkSourceSearchContext *search) { if (search->priv->idle_scan_id == 0) { search->priv->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->priv->found_tag)) { gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag); } else if (!gtk_text_iter_starts_tag (&iter, search->priv->found_tag)) { gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag); region_start = iter; } limit = iter; gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->found_tag); if (search->priv->scan_region != NULL) { region = gtk_source_region_intersect_subregion (search->priv->scan_region, ®ion_start, &limit); } if (gtk_source_region_is_empty (region)) { g_clear_object (®ion); 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->priv->settings)) { regex_search_scan_next_chunk (search); } else { scan_region_forward (search, region); } g_clear_object (®ion); 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->priv->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->priv->found_tag) || (!gtk_text_iter_has_tag (&iter, search->priv->found_tag) && !gtk_text_iter_ends_tag (&iter, search->priv->found_tag))) { gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag); } else if (gtk_text_iter_has_tag (&iter, search->priv->found_tag)) { gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag); region_end = iter; } limit = iter; gtk_text_iter_backward_to_tag_toggle (&limit, search->priv->found_tag); if (search->priv->scan_region != NULL) { region = gtk_source_region_intersect_subregion (search->priv->scan_region, &limit, ®ion_end); } if (gtk_source_region_is_empty (region)) { g_clear_object (®ion); 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->priv->settings)) { regex_search_scan_next_chunk (search); } else { scan_region_forward (search, region); } g_clear_object (®ion); 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->priv->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->priv->scan_region == NULL) { search->priv->scan_region = gtk_source_region_new (search->priv->buffer); } DEBUG ({ g_print ("add_subregion_to_scan(): region to scan, before:\n"); print_region (search->priv->scan_region); }); gtk_source_region_add_subregion (search->priv->scan_region, &start, &end); DEBUG ({ g_print ("add_subregion_to_scan(): region to scan, after:\n"); print_region (search->priv->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->priv->settings); if (search->priv->regex != NULL) { g_regex_unref (search->priv->regex); search->priv->regex = NULL; } if (search->priv->regex_error != NULL) { g_clear_error (&search->priv->regex_error); regex_error_changed = TRUE; } if (search_text != NULL && gtk_source_search_settings_get_regex_enabled (search->priv->settings)) { GRegexCompileFlags compile_flags = G_REGEX_OPTIMIZE | G_REGEX_MULTILINE; gchar *pattern = (gchar *)search_text; search->priv->text_nb_lines = 0; if (!gtk_source_search_settings_get_case_sensitive (search->priv->settings)) { compile_flags |= G_REGEX_CASELESS; } if (gtk_source_search_settings_get_at_word_boundaries (search->priv->settings)) { pattern = g_strdup_printf ("\\b%s\\b", search_text); } search->priv->regex = g_regex_new (pattern, compile_flags, G_REGEX_MATCH_NOTEMPTY, &search->priv->regex_error); if (search->priv->regex_error != NULL) { regex_error_changed = TRUE; } if (gtk_source_search_settings_get_at_word_boundaries (search->priv->settings)) { g_free (pattern); } } if (regex_error_changed) { g_object_notify (G_OBJECT (search), "regex-error"); } } static void update (GtkSourceSearchContext *search) { GtkTextIter start; GtkTextIter end; GtkSourceBufferInternal *buffer_internal; if (search->priv->buffer == NULL) { return; } clear_search (search); update_regex (search); search->priv->scan_region = gtk_source_region_new (search->priv->buffer); gtk_text_buffer_get_bounds (search->priv->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->priv->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->priv->settings); clear_task (search); if (search_text != NULL && !gtk_source_search_settings_get_regex_enabled (search->priv->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->priv->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->priv->settings); clear_task (search); if (gtk_source_search_settings_get_regex_enabled (search->priv->settings)) { return; } gtk_text_buffer_get_bounds (search->priv->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->priv->occurrences_count = 0; return; } if (search_text != NULL) { GtkTextIter start = *delete_start; GtkTextIter end = *delete_end; gtk_text_iter_backward_lines (&start, search->priv->text_nb_lines); gtk_text_iter_forward_lines (&end, search->priv->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->priv->settings)) { update (search); } else { add_subregion_to_scan (search, start, end); } } static void set_buffer (GtkSourceSearchContext *search, GtkSourceBuffer *buffer) { g_assert (search->priv->buffer == NULL); g_assert (search->priv->tag_table == NULL); search->priv->buffer = GTK_TEXT_BUFFER (buffer); g_object_add_weak_pointer (G_OBJECT (buffer), (gpointer *)&search->priv->buffer); search->priv->tag_table = gtk_text_buffer_get_tag_table (search->priv->buffer); g_object_ref (search->priv->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->priv->found_tag = gtk_text_buffer_create_tag (search->priv->buffer, NULL, NULL); g_object_ref (search->priv->found_tag); sync_found_tag (search); g_signal_connect_object (search->priv->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->priv->settings)) { search->priv->text_nb_lines = 0; } else { const gchar *text = gtk_source_search_settings_get_search_text (search->priv->settings); search->priv->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 gtk_source_search_context_dispose (GObject *object) { GtkSourceSearchContext *search = GTK_SOURCE_SEARCH_CONTEXT (object); clear_search (search); if (search->priv->found_tag != NULL && search->priv->tag_table != NULL) { gtk_text_tag_table_remove (search->priv->tag_table, search->priv->found_tag); g_clear_object (&search->priv->found_tag); g_clear_object (&search->priv->tag_table); } if (search->priv->buffer != NULL) { g_object_remove_weak_pointer (G_OBJECT (search->priv->buffer), (gpointer *)&search->priv->buffer); search->priv->buffer = NULL; } g_clear_object (&search->priv->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); if (search->priv->regex != NULL) { g_regex_unref (search->priv->regex); } g_clear_error (&search->priv->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->priv->buffer); break; case PROP_SETTINGS: g_value_set_object (value, search->priv->settings); break; case PROP_HIGHLIGHT: g_value_set_boolean (value, search->priv->highlight); break; case PROP_MATCH_STYLE: g_value_set_object (value, search->priv->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_set_pointer (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: G_GNUC_BEGIN_IGNORE_DEPRECATIONS; gtk_source_search_context_set_settings (search, g_value_get_object (value)); G_GNUC_END_IGNORE_DEPRECATIONS; 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 #GtkSourceBuffer associated to the search context. * * Since: 3.10 */ g_object_class_install_property (object_class, 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 #GtkSourceSearchSettings associated to the search context. * * Since: 3.10 */ g_object_class_install_property (object_class, PROP_SETTINGS, g_param_spec_object ("settings", "Settings", "The associated GtkSourceSearchSettings", GTK_SOURCE_TYPE_SEARCH_SETTINGS, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); /** * GtkSourceSearchContext:highlight: * * Highlight the search occurrences. * * Since: 3.10 */ g_object_class_install_property (object_class, 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 #GtkSourceStyle, or %NULL for theme's scheme default style. * * Since: 3.16 */ g_object_class_install_property (object_class, 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. * * Since: 3.10 */ g_object_class_install_property (object_class, 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 * property will be set. If the pattern is valid, the value is %NULL. * * Free with g_error_free(). * * Since: 3.10 */ g_object_class_install_property (object_class, PROP_REGEX_ERROR, g_param_spec_pointer ("regex-error", "Regex error", "Regular expression error", G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); } static void gtk_source_search_context_init (GtkSourceSearchContext *search) { search->priv = 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 #GtkSourceSearchSettings object will * be created, that you can retrieve with * gtk_source_search_context_get_settings(). * * Returns: a new search context. * Since: 3.10 */ 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. * Since: 3.10 */ 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->priv->buffer); } /** * gtk_source_search_context_get_settings: * @search: a #GtkSourceSearchContext. * * Returns: (transfer none): the search settings. * Since: 3.10 */ GtkSourceSearchSettings * gtk_source_search_context_get_settings (GtkSourceSearchContext *search) { g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL); return search->priv->settings; } /** * gtk_source_search_context_set_settings: * @search: a #GtkSourceSearchContext. * @settings: (nullable): the new #GtkSourceSearchSettings, or %NULL. * * Associate a #GtkSourceSearchSettings with the search context. If @settings is * %NULL, a new one will be created. * * The search context holds a reference to @settings. * * Since: 3.10 * Deprecated: 3.24: The #GtkSourceSearchContext:settings property will become a * construct-only property in a future version. Create a new * #GtkSourceSearchContext instead, or change the #GtkSourceSearchSettings * properties. When the #GtkSourceSearchContext:settings property will become * construct-only, it will be possible to simplify some code that needed to * listen to the notify::settings signal. */ void gtk_source_search_context_set_settings (GtkSourceSearchContext *search, GtkSourceSearchSettings *settings) { g_return_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search)); g_return_if_fail (settings == NULL || GTK_SOURCE_IS_SEARCH_SETTINGS (settings)); if (search->priv->settings != NULL) { g_warning ("%s() is deprecated, the GtkSourceSearchContext:settings property " "will become a construct-only property in a future version.", G_STRFUNC); g_signal_handlers_disconnect_by_func (search->priv->settings, settings_notify_cb, search); g_object_unref (search->priv->settings); } if (settings != NULL) { search->priv->settings = g_object_ref (settings); } else { search->priv->settings = gtk_source_search_settings_new (); } g_signal_connect_object (search->priv->settings, "notify", G_CALLBACK (settings_notify_cb), search, G_CONNECT_SWAPPED); search_text_updated (search); update (search); g_object_notify (G_OBJECT (search), "settings"); } /** * gtk_source_search_context_get_highlight: * @search: a #GtkSourceSearchContext. * * Returns: whether to highlight the search occurrences. * Since: 3.10 */ gboolean gtk_source_search_context_get_highlight (GtkSourceSearchContext *search) { g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), FALSE); return search->priv->highlight; } /** * gtk_source_search_context_set_highlight: * @search: a #GtkSourceSearchContext. * @highlight: the setting. * * Enables or disables the search occurrences highlighting. * * Since: 3.10 */ 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->priv->highlight != highlight) { search->priv->highlight = highlight; sync_found_tag (search); g_object_notify (G_OBJECT (search), "highlight"); } } /** * gtk_source_search_context_get_match_style: * @search: a #GtkSourceSearchContext. * * Returns: (transfer none): the #GtkSourceStyle to apply on search matches. * * Since: 3.16 */ GtkSourceStyle * gtk_source_search_context_get_match_style (GtkSourceSearchContext *search) { g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL); return search->priv->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 * gtk_source_search_context_set_highlight(). * * Since: 3.16 */ 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->priv->match_style == match_style) { return; } if (search->priv->match_style != NULL) { g_object_unref (search->priv->match_style); } search->priv->match_style = match_style; if (match_style != NULL) { g_object_ref (match_style); } g_object_notify (G_OBJECT (search), "match-style"); } /** * gtk_source_search_context_get_regex_error: * @search: a #GtkSourceSearchContext. * * Regular expression patterns must follow certain rules. If * #GtkSourceSearchSettings:search-text breaks a rule, the error can be retrieved * with this function. The error domain is #G_REGEX_ERROR. * * Free the return value with g_error_free(). * * Returns: (nullable): the #GError, or %NULL if the pattern is valid. * Since: 3.10 */ GError * gtk_source_search_context_get_regex_error (GtkSourceSearchContext *search) { g_return_val_if_fail (GTK_SOURCE_IS_SEARCH_CONTEXT (search), NULL); if (search->priv->regex_error == NULL) { return NULL; } return g_error_copy (search->priv->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. * Since: 3.10 */ 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->priv->scan_region)) { return -1; } return search->priv->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. * * Since: 3.10 */ 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->priv->buffer == NULL) { return -1; } /* Verify that the [match_start; match_end] region has been scanned. */ if (search->priv->scan_region != NULL) { region = gtk_source_region_intersect_subregion (search->priv->scan_region, match_start, match_end); empty = gtk_source_region_is_empty (region); g_clear_object (®ion); 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->priv->buffer, &iter); if (search->priv->scan_region != NULL) { region = gtk_source_region_intersect_subregion (search->priv->scan_region, &iter, match_end); empty = gtk_source_region_is_empty (region); g_clear_object (®ion); 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. * * 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. * * Returns: whether a match was found. * Since: 3.10 * Deprecated: 3.22: Use gtk_source_search_context_forward2() instead. */ gboolean gtk_source_search_context_forward (GtkSourceSearchContext *search, const GtkTextIter *iter, GtkTextIter *match_start, GtkTextIter *match_end) { return gtk_source_search_context_forward2 (search, iter, match_start, match_end, NULL); } /** * gtk_source_search_context_forward2: * @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. * * The difference with gtk_source_search_context_forward() is that the * @has_wrapped_around out parameter has been added for convenience. * * If the #GtkSourceSearchSettings: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 #GtkSourceSearchSettings:wrap-around property. * * Returns: whether a match was found. * Since: 3.22 */ gboolean gtk_source_search_context_forward2 (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->priv->buffer == NULL) { return FALSE; } found = smart_forward_search (search, iter, &m_start, &m_end); if (!found && gtk_source_search_settings_get_wrap_around (search->priv->settings)) { GtkTextIter start_iter; gtk_text_buffer_get_start_iter (search->priv->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 gtk_source_search_context_forward2(). * * See the documentation of gtk_source_search_context_forward2() for more * details. * * See the #GAsyncResult documentation to know how to use this function. * * If the operation is cancelled, the @callback will only be called if * @cancellable was not %NULL. gtk_source_search_context_forward_async() takes * ownership of @cancellable, so you can unref it after calling this function. * * Since: 3.10 */ 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->priv->buffer == NULL) { return; } clear_task (search); search->priv->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. * @error: a #GError, or %NULL. * * Finishes a forward search started with * gtk_source_search_context_forward_async(). * * Returns: whether a match was found. * Since: 3.10 * Deprecated: 3.22: Use gtk_source_search_context_forward_finish2() instead. */ gboolean gtk_source_search_context_forward_finish (GtkSourceSearchContext *search, GAsyncResult *result, GtkTextIter *match_start, GtkTextIter *match_end, GError **error) { return gtk_source_search_context_forward_finish2 (search, result, match_start, match_end, NULL, error); } /** * gtk_source_search_context_forward_finish2: * @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 * gtk_source_search_context_forward_async(). * * See the documentation of gtk_source_search_context_forward2() for more * details. * * Returns: whether a match was found. * Since: 3.22 */ gboolean gtk_source_search_context_forward_finish2 (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->priv->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) { *match_start = data->match_start; } if (match_end != NULL) { *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. * * 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. * * Returns: whether a match was found. * Since: 3.10 * Deprecated: 3.22: Use gtk_source_search_context_backward2() instead. */ gboolean gtk_source_search_context_backward (GtkSourceSearchContext *search, const GtkTextIter *iter, GtkTextIter *match_start, GtkTextIter *match_end) { return gtk_source_search_context_backward2 (search, iter, match_start, match_end, NULL); } /** * gtk_source_search_context_backward2: * @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. * * The difference with gtk_source_search_context_backward() is that the * @has_wrapped_around out parameter has been added for convenience. * * If the #GtkSourceSearchSettings: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 #GtkSourceSearchSettings:wrap-around property. * * Returns: whether a match was found. * Since: 3.22 */ gboolean gtk_source_search_context_backward2 (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->priv->buffer == NULL) { return FALSE; } found = smart_backward_search (search, iter, &m_start, &m_end); if (!found && gtk_source_search_settings_get_wrap_around (search->priv->settings)) { GtkTextIter end_iter; gtk_text_buffer_get_end_iter (search->priv->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 gtk_source_search_context_backward2(). * * See the documentation of gtk_source_search_context_backward2() for more * details. * * See the #GAsyncResult documentation to know how to use this function. * * If the operation is cancelled, the @callback will only be called if * @cancellable was not %NULL. gtk_source_search_context_backward_async() takes * ownership of @cancellable, so you can unref it after calling this function. * * Since: 3.10 */ 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->priv->buffer == NULL) { return; } clear_task (search); search->priv->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. * @error: a #GError, or %NULL. * * Finishes a backward search started with * gtk_source_search_context_backward_async(). * * Returns: whether a match was found. * Since: 3.10 * Deprecated: 3.22: Use gtk_source_search_context_backward_finish2() instead. */ gboolean gtk_source_search_context_backward_finish (GtkSourceSearchContext *search, GAsyncResult *result, GtkTextIter *match_start, GtkTextIter *match_end, GError **error) { return gtk_source_search_context_forward_finish2 (search, result, match_start, match_end, NULL, error); } /** * gtk_source_search_context_backward_finish2: * @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 * gtk_source_search_context_backward_async(). * * See the documentation of gtk_source_search_context_backward2() for more * details. * * Returns: whether a match was found. * Since: 3.22 */ gboolean gtk_source_search_context_backward_finish2 (GtkSourceSearchContext *search, GAsyncResult *result, GtkTextIter *match_start, GtkTextIter *match_end, gboolean *has_wrapped_around, GError **error) { return gtk_source_search_context_forward_finish2 (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->priv->regex == NULL || search->priv->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 = g_regex_replace (search->priv->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->priv->buffer); gtk_text_buffer_delete (search->priv->buffer, &match_start_copy, match_end); gtk_text_buffer_insert (search->priv->buffer, match_end, subject_replaced + start_pos, -1); gtk_text_buffer_end_user_action (search->priv->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. * * For a regular expression replacement, you can check if @replace is valid by * calling g_regex_check_replacement(). The @replace text can contain * backreferences; read the g_regex_replace() documentation for more details. * * Returns: whether the match has been replaced. * Since: 3.10 * Deprecated: 3.22: Use gtk_source_search_context_replace2() instead. */ gboolean gtk_source_search_context_replace (GtkSourceSearchContext *search, const GtkTextIter *match_start, const GtkTextIter *match_end, const gchar *replace, gint replace_length, GError **error) { GtkTextIter start; GtkTextIter end; g_return_val_if_fail (match_start != NULL, FALSE); g_return_val_if_fail (match_end != NULL, FALSE); start = *match_start; end = *match_end; return gtk_source_search_context_replace2 (search, &start, &end, replace, replace_length, error); } /** * gtk_source_search_context_replace2: * @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. * * Unlike with gtk_source_search_context_replace(), the @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 g_regex_check_replacement(). The @replace text can contain * backreferences; read the g_regex_replace() documentation for more details. * * Returns: whether the match has been replaced. * Since: 3.22 */ gboolean gtk_source_search_context_replace2 (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->priv->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->priv->buffer, NULL, &start, TRUE); if (gtk_source_search_settings_get_regex_enabled (search->priv->settings)) { replaced = regex_replace (search, &start, &end, replace, error); } else { gtk_text_buffer_begin_user_action (search->priv->buffer); gtk_text_buffer_delete (search->priv->buffer, &start, &end); gtk_text_buffer_insert (search->priv->buffer, &end, replace, replace_length); gtk_text_buffer_end_user_action (search->priv->buffer); replaced = TRUE; } if (replaced) { gtk_text_buffer_get_iter_at_mark (search->priv->buffer, match_start, start_mark); *match_end = end; } gtk_text_buffer_delete_mark (search->priv->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 g_regex_check_replacement(). The @replace text can contain * backreferences; read the g_regex_replace() documentation for more details. * * Returns: the number of replaced matches. * Since: 3.10 */ 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->priv->buffer == NULL) { return 0; } if (gtk_source_search_settings_get_regex_enabled (search->priv->settings)) { GError *tmp_error = NULL; if (search->priv->regex == NULL || search->priv->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->priv->buffer, insert_text_before_cb, search); g_signal_handlers_block_by_func (search->priv->buffer, insert_text_after_cb, search); g_signal_handlers_block_by_func (search->priv->buffer, delete_range_before_cb, search); g_signal_handlers_block_by_func (search->priv->buffer, delete_range_after_cb, search); highlight_matching_brackets = gtk_source_buffer_get_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->priv->buffer)); gtk_source_buffer_set_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->priv->buffer), FALSE); _gtk_source_buffer_save_and_clear_selection (GTK_SOURCE_BUFFER (search->priv->buffer)); gtk_text_buffer_get_start_iter (search->priv->buffer, &iter); gtk_text_buffer_begin_user_action (search->priv->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->priv->buffer, &match_start, &match_end); gtk_text_buffer_insert (search->priv->buffer, &match_end, replace, replace_length); } nb_matches_replaced++; iter = match_end; } gtk_text_buffer_end_user_action (search->priv->buffer); _gtk_source_buffer_restore_selection (GTK_SOURCE_BUFFER (search->priv->buffer)); gtk_source_buffer_set_highlight_matching_brackets (GTK_SOURCE_BUFFER (search->priv->buffer), highlight_matching_brackets); g_signal_handlers_unblock_by_func (search->priv->buffer, insert_text_before_cb, search); g_signal_handlers_unblock_by_func (search->priv->buffer, insert_text_after_cb, search); g_signal_handlers_unblock_by_func (search->priv->buffer, delete_range_before_cb, search); g_signal_handlers_unblock_by_func (search->priv->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->priv->buffer == NULL || gtk_source_region_is_empty (search->priv->scan_region) || !search->priv->highlight) { return; } region_to_highlight = gtk_source_region_intersect_subregion (search->priv->scan_region, start, end); if (gtk_source_region_is_empty (region_to_highlight)) { goto out; } if (!synchronous) { if (search->priv->high_priority_region == NULL) { search->priv->high_priority_region = region_to_highlight; region_to_highlight = NULL; } else { gtk_source_region_add_region (search->priv->high_priority_region, region_to_highlight); } install_idle_scan (search); goto out; } if (gtk_source_search_settings_get_regex_enabled (search->priv->settings)) { GtkTextIter region_start; if (!gtk_source_region_get_bounds (search->priv->scan_region, ®ion_start, NULL)) { goto out; } regex_search_scan_chunk (search, ®ion_start, end); } else { scan_all_region (search, region_to_highlight); } out: g_clear_object (®ion_to_highlight); }