[gtksourceview/wip/search: 8/8] Higher-level asynchronous search and replace API



commit 968f35ff520b8875865814a5d2ed32df7fdb1482
Author: Sébastien Wilmet <swilmet gnome org>
Date:   Sat Jun 15 10:46:39 2013 +0200

    Higher-level asynchronous search and replace API

 docs/reference/Makefile.am                    |    1 +
 docs/reference/gtksourceview-3.0-sections.txt |   13 +
 docs/reference/gtksourceview-docs.xml         |    1 +
 gtksourceview/Makefile.am                     |    4 +
 gtksourceview/gtksource.h                     |    1 +
 gtksourceview/gtksourcebuffer.c               |  205 +++++-
 gtksourceview/gtksourcebuffer.h               |   27 +
 gtksourceview/gtksourcesearch.c               | 1092 +++++++++++++++++++++++++
 gtksourceview/gtksourcesearch.h               |   84 ++
 gtksourceview/gtksourcetypes-private.h        |    1 +
 gtksourceview/gtksourceutils.c                |  189 +++++
 gtksourceview/gtksourceutils.h                |   34 +
 po/POTFILES.in                                |    2 +
 tests/Makefile.am                             |   33 +-
 tests/test-search-ui.c                        |  169 ++++
 tests/test-search-ui.gresource.xml            |    6 +
 tests/test-search-ui.ui                       |  133 +++
 tests/test-search.c                           |  237 ++++++
 18 files changed, 2226 insertions(+), 6 deletions(-)
---
diff --git a/docs/reference/Makefile.am b/docs/reference/Makefile.am
index 0ee0152..58d5593 100644
--- a/docs/reference/Makefile.am
+++ b/docs/reference/Makefile.am
@@ -37,6 +37,7 @@ IGNORE_HFILES =                                       \
        gtksourcelanguage-private.h             \
        gtksourcepixbufhelper.h                 \
        gtksourceregex.h                        \
+       gtksourcesearch.h                       \
        gtksourcestyle-private.h                \
        gtksourcetypes-private.h                \
        gtksourceundomanagerdefault.h           \
diff --git a/docs/reference/gtksourceview-3.0-sections.txt b/docs/reference/gtksourceview-3.0-sections.txt
index 1e82636..47fc1d1 100644
--- a/docs/reference/gtksourceview-3.0-sections.txt
+++ b/docs/reference/gtksourceview-3.0-sections.txt
@@ -5,6 +5,7 @@
 <TITLE>GtkSourceBuffer</TITLE>
 GtkSourceBuffer
 GtkSourceBracketMatchType
+GtkSourceSearchFlags
 gtk_source_buffer_new
 gtk_source_buffer_new_with_language
 gtk_source_buffer_set_highlight_syntax
@@ -36,6 +37,11 @@ gtk_source_buffer_iter_forward_to_context_class_toggle
 gtk_source_buffer_iter_backward_to_context_class_toggle
 gtk_source_buffer_get_undo_manager
 gtk_source_buffer_set_undo_manager
+gtk_source_buffer_set_search_text
+gtk_source_buffer_get_search_text
+gtk_source_buffer_set_search_flags
+gtk_source_buffer_get_search_flags
+gtk_source_buffer_get_search_occurrences_count
 <SUBSECTION Standard>
 GtkSourceBufferClass
 GTK_SOURCE_IS_BUFFER
@@ -600,3 +606,10 @@ GtkSourceCompletionWordsClass
 GtkSourceCompletionWordsPrivate
 gtk_source_completion_words_get_type
 </SECTION>
+
+<SECTION>
+<FILE>utils</FILE>
+<TITLE>GtkSourceUtils</TITLE>
+gtk_source_utils_unescape_search_text
+gtk_source_utils_escape_search_text
+</SECTION>
diff --git a/docs/reference/gtksourceview-docs.xml b/docs/reference/gtksourceview-docs.xml
index abcdc61..90fee63 100644
--- a/docs/reference/gtksourceview-docs.xml
+++ b/docs/reference/gtksourceview-docs.xml
@@ -37,6 +37,7 @@
     <xi:include href="xml/stylescheme.xml"/>
     <xi:include href="xml/styleschememanager.xml"/>
     <xi:include href="xml/undomanager.xml"/>
+    <xi:include href="xml/utils.xml"/>
     <xi:include href="xml/view.xml"/>
   </chapter>
 
diff --git a/gtksourceview/Makefile.am b/gtksourceview/Makefile.am
index 269854e..88e281f 100644
--- a/gtksourceview/Makefile.am
+++ b/gtksourceview/Makefile.am
@@ -44,6 +44,7 @@ libgtksourceview_headers =                    \
        gtksourcestyleschememanager.h           \
        gtksourcetypes.h                        \
        gtksourceundomanager.h                  \
+       gtksourceutils.h                        \
        gtksourceview.h
 
 libgtksourceview_private_headers = \
@@ -59,6 +60,7 @@ libgtksourceview_private_headers = \
        gtksourcelanguage-private.h             \
        gtksourcepixbufhelper.h                 \
        gtksourceregex.h                        \
+       gtksourcesearch.h                       \
        gtksourcestyle-private.h                \
        gtksourcetypes-private.h                \
        gtksourceundomanagerdefault.h           \
@@ -75,6 +77,7 @@ libgtksourceview_private_c_files = \
        gtksourcelanguage-parser-1.c    \
        gtksourcelanguage-parser-2.c    \
        gtksourceregex.c                \
+       gtksourcesearch.c               \
        gtksourceundomanager.c          \
        gtksourceundomanagerdefault.c   \
        gtksourceview-i18n.c            \
@@ -103,6 +106,7 @@ libgtksourceview_c_files = \
        gtksourcestyle.c                \
        gtksourcestylescheme.c          \
        gtksourcestyleschememanager.c   \
+       gtksourceutils.c                \
        gtksourceview.c
 
 # Split in a helper library for unit tests
diff --git a/gtksourceview/gtksource.h b/gtksourceview/gtksource.h
index 31a5919..d95a037 100644
--- a/gtksourceview/gtksource.h
+++ b/gtksourceview/gtksource.h
@@ -41,6 +41,7 @@
 #include <gtksourceview/gtksourcestylescheme.h>
 #include <gtksourceview/gtksourcestyleschememanager.h>
 #include <gtksourceview/gtksourceundomanager.h>
+#include <gtksourceview/gtksourceutils.h>
 #include <gtksourceview/gtksourceview.h>
 #include <gtksourceview/gtksourceview-typebuiltins.h>
 
diff --git a/gtksourceview/gtksourcebuffer.c b/gtksourceview/gtksourcebuffer.c
index 4828fc5..8b2532b 100644
--- a/gtksourceview/gtksourcebuffer.c
+++ b/gtksourceview/gtksourcebuffer.c
@@ -42,6 +42,7 @@
 #include "gtksourceundomanagerdefault.h"
 #include "gtksourceview-typebuiltins.h"
 #include "gtksourcemark.h"
+#include "gtksourcesearch.h"
 
 /**
  * SECTION:buffer
@@ -120,7 +121,10 @@ enum {
        PROP_MAX_UNDO_LEVELS,
        PROP_LANGUAGE,
        PROP_STYLE_SCHEME,
-       PROP_UNDO_MANAGER
+       PROP_UNDO_MANAGER,
+       PROP_SEARCH_TEXT,
+       PROP_SEARCH_FLAGS,
+       PROP_SEARCH_OCCURRENCES_COUNT
 };
 
 struct _GtkSourceBufferPrivate
@@ -140,6 +144,8 @@ struct _GtkSourceBufferPrivate
        GtkSourceUndoManager  *undo_manager;
        gint                   max_undo_levels;
 
+       GtkSourceSearch       *search;
+
        guint                  highlight_syntax : 1;
        guint                  highlight_brackets : 1;
        guint                  constructed : 1;
@@ -335,6 +341,55 @@ gtk_source_buffer_class_init (GtkSourceBufferClass *klass)
                                                              GTK_SOURCE_TYPE_UNDO_MANAGER,
                                                              G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
 
+       /**
+        * GtkSourceBuffer:search-text:
+        *
+        * A search string, or %NULL if the search is disabled.
+        *
+        * Since: 3.10
+        */
+       g_object_class_install_property (object_class,
+                                        PROP_SEARCH_TEXT,
+                                        g_param_spec_string ("search-text",
+                                                             _("Search text"),
+                                                             _("The text to search"),
+                                                             NULL,
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+       /**
+        * GtkSourceBuffer:search-flags:
+        *
+        * Flags affecting how the search is done.
+        *
+        * Since: 3.10
+        */
+       g_object_class_install_property (object_class,
+                                        PROP_SEARCH_FLAGS,
+                                        g_param_spec_flags ("search-flags",
+                                                            _("Search flags"),
+                                                            _("Search flags"),
+                                                            GTK_SOURCE_TYPE_SEARCH_FLAGS,
+                                                            0,
+                                                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+       /**
+        * GtkSourceBuffer:search-occurrences-count:
+        *
+        * The total number of search occurrences. If the search is disabled,
+        * the value is 0.
+        *
+        * Since: 3.10
+        */
+       g_object_class_install_property (object_class,
+                                        PROP_SEARCH_OCCURRENCES_COUNT,
+                                        g_param_spec_uint ("search-occurrences-count",
+                                                           _("Search occurrences count"),
+                                                           _("Total number of search occurrences"),
+                                                           0,
+                                                           G_MAXUINT,
+                                                           0,
+                                                           G_PARAM_READABLE));
+
        param_types[0] = GTK_TYPE_TEXT_ITER | G_SIGNAL_TYPE_STATIC_SCOPE;
        param_types[1] = GTK_TYPE_TEXT_ITER | G_SIGNAL_TYPE_STATIC_SCOPE;
 
@@ -476,6 +531,8 @@ gtk_source_buffer_init (GtkSourceBuffer *buffer)
 
        if (priv->style_scheme != NULL)
                g_object_ref (priv->style_scheme);
+
+       priv->search = _gtk_source_search_new (buffer);
 }
 
 static void
@@ -518,6 +575,7 @@ gtk_source_buffer_dispose (GObject *object)
        g_clear_object (&buffer->priv->highlight_engine);
        g_clear_object (&buffer->priv->language);
        g_clear_object (&buffer->priv->style_scheme);
+       g_clear_object (&buffer->priv->search);
 
        G_OBJECT_CLASS (gtk_source_buffer_parent_class)->dispose (object);
 }
@@ -566,6 +624,16 @@ gtk_source_buffer_set_property (GObject      *object,
                                                            g_value_get_object (value));
                        break;
 
+               case PROP_SEARCH_TEXT:
+                       _gtk_source_search_set_text (source_buffer->priv->search,
+                                                    g_value_get_string (value));
+                       break;
+
+               case PROP_SEARCH_FLAGS:
+                       _gtk_source_search_set_flags (source_buffer->priv->search,
+                                                     g_value_get_flags (value));
+                       break;
+
                default:
                        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
                        break;
@@ -621,6 +689,18 @@ gtk_source_buffer_get_property (GObject    *object,
                        g_value_set_object (value, source_buffer->priv->undo_manager);
                        break;
 
+               case PROP_SEARCH_TEXT:
+                       g_value_set_string (value, _gtk_source_search_get_text (source_buffer->priv->search));
+                       break;
+
+               case PROP_SEARCH_FLAGS:
+                       g_value_set_flags (value, _gtk_source_search_get_flags (source_buffer->priv->search));
+                       break;
+
+               case PROP_SEARCH_OCCURRENCES_COUNT:
+                       g_value_set_uint (value, _gtk_source_search_get_occurrences_count 
(source_buffer->priv->search));
+                       break;
+
                default:
                        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
                        break;
@@ -1532,10 +1612,17 @@ _gtk_source_buffer_update_highlight (GtkSourceBuffer   *buffer,
        g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
 
        if (buffer->priv->highlight_engine != NULL)
+       {
                _gtk_source_engine_update_highlight (buffer->priv->highlight_engine,
                                                     start,
                                                     end,
                                                     synchronous);
+       }
+
+       _gtk_source_search_update_highlight (buffer->priv->search,
+                                            start,
+                                            end,
+                                            synchronous);
 }
 
 /**
@@ -2476,3 +2563,119 @@ gtk_source_buffer_get_undo_manager (GtkSourceBuffer *buffer)
 
        return buffer->priv->undo_manager;
 }
+
+/**
+ * gtk_source_buffer_set_search_text:
+ * @buffer: a #GtkSourceBuffer.
+ * @text: the text to search, or %NULL to disable the search.
+ *
+ * Sets the text to search. If @text is %NULL or is empty, the search will be
+ * disabled. A copy of @text will be made, so you can safely free @text after
+ * a call to this function.
+ *
+ * You may be interested to call gtk_source_utils_unescape_search_text() before
+ * this function.
+ *
+ * Since: 3.10
+ */
+void
+gtk_source_buffer_set_search_text (GtkSourceBuffer *buffer,
+                                  const gchar     *text)
+{
+       const gchar *cur_text;
+
+       g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
+
+       cur_text = _gtk_source_search_get_text (buffer->priv->search);
+
+       if (cur_text == NULL && (text == NULL || *text == '\0'))
+       {
+               return;
+       }
+
+       if (g_strcmp0 (cur_text, text) != 0)
+       {
+               _gtk_source_search_set_text (buffer->priv->search, text);
+               g_object_notify (G_OBJECT (buffer), "search-text");
+       }
+}
+
+/**
+ * gtk_source_buffer_get_search_text:
+ * @buffer: a #GtkSourceBuffer.
+ *
+ * Gets the text to search. The return value must not be freed.
+ *
+ * You may be interested to call gtk_source_utils_escape_search_text() after
+ * this function.
+ *
+ * Returns: the text to search, or %NULL if the search is disabled.
+ * Since: 3.10
+ */
+const gchar *
+gtk_source_buffer_get_search_text (GtkSourceBuffer *buffer)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), NULL);
+
+       return _gtk_source_search_get_text (buffer->priv->search);
+}
+
+/**
+ * gtk_source_buffer_set_search_flags:
+ * @buffer: a #GtkSourceBuffer.
+ * @flags: flags affecting how the search is done.
+ *
+ * Modifies how the search is done.
+ *
+ * Since: 3.10
+ */
+void
+gtk_source_buffer_set_search_flags (GtkSourceBuffer      *buffer,
+                                   GtkSourceSearchFlags  flags)
+{
+       GtkSourceSearchFlags cur_flags;
+
+       g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
+
+       cur_flags = _gtk_source_search_get_flags (buffer->priv->search);
+
+       if (cur_flags != flags)
+       {
+               _gtk_source_search_set_flags (buffer->priv->search, flags);
+               g_object_notify (G_OBJECT (buffer), "search-flags");
+       }
+}
+
+/**
+ * gtk_source_buffer_get_search_flags:
+ * @buffer: a #GtkSourceBuffer.
+ *
+ * Gets the search flags.
+ *
+ * Returns: the flags affecting how the search is done.
+ * Since: 3.10
+ */
+GtkSourceSearchFlags
+gtk_source_buffer_get_search_flags (GtkSourceBuffer *buffer)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), 0);
+
+       return _gtk_source_search_get_flags (buffer->priv->search);
+}
+
+/**
+ * gtk_source_buffer_get_search_occurrences_count:
+ * @buffer: a #GtkSourceBuffer.
+ *
+ * Gets the total number of search occurrences.
+ *
+ * Returns: the total number of search occurrences.
+ * Since: 3.10
+ */
+guint
+gtk_source_buffer_get_search_occurrences_count (GtkSourceBuffer *buffer)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), 0);
+
+       return _gtk_source_search_get_occurrences_count (buffer->priv->search);
+}
diff --git a/gtksourceview/gtksourcebuffer.h b/gtksourceview/gtksourcebuffer.h
index 80b365e..9ce9b3e 100644
--- a/gtksourceview/gtksourcebuffer.h
+++ b/gtksourceview/gtksourcebuffer.h
@@ -56,6 +56,20 @@ typedef enum
        GTK_SOURCE_BRACKET_MATCH_FOUND
 } GtkSourceBracketMatchType;
 
+/**
+ * GtkSourceSearchFlags:
+ * @GTK_SOURCE_SEARCH_CASE_SENSITIVE: Case sensitive search.
+ *
+ * Flags affecting how the search is done. Internally, GtkSourceView always
+ * enables #GTK_TEXT_SEARCH_VISIBLE_ONLY and #GTK_TEXT_SEARCH_TEXT_ONLY.
+ *
+ * Since: 3.10
+ */
+typedef enum
+{
+       GTK_SOURCE_SEARCH_CASE_SENSITIVE        = 1 << 0
+} GtkSourceSearchFlags;
+
 struct _GtkSourceBuffer
 {
        GtkTextBuffer parent_instance;
@@ -175,6 +189,19 @@ GtkSourceUndoManager       *gtk_source_buffer_get_undo_manager     (GtkSourceBuffer      
  *buffe
 void                    gtk_source_buffer_set_undo_manager     (GtkSourceBuffer        *buffer,
                                                                 GtkSourceUndoManager   *manager);
 
+void                    gtk_source_buffer_set_search_text      (GtkSourceBuffer        *buffer,
+                                                                const gchar            *text);
+
+const gchar            *gtk_source_buffer_get_search_text      (GtkSourceBuffer        *buffer);
+
+void                    gtk_source_buffer_set_search_flags     (GtkSourceBuffer        *buffer,
+                                                                GtkSourceSearchFlags    flags);
+
+GtkSourceSearchFlags    gtk_source_buffer_get_search_flags     (GtkSourceBuffer        *buffer);
+
+guint                   gtk_source_buffer_get_search_occurrences_count
+                                                               (GtkSourceBuffer        *buffer);
+
 /* private */
 void                    _gtk_source_buffer_update_highlight    (GtkSourceBuffer        *buffer,
                                                                 const GtkTextIter      *start,
diff --git a/gtksourceview/gtksourcesearch.c b/gtksourceview/gtksourcesearch.c
new file mode 100644
index 0000000..2805f6d
--- /dev/null
+++ b/gtksourceview/gtksourcesearch.c
@@ -0,0 +1,1092 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */
+/* gtksourcesearch.c
+ * This file is part of GtkSourceView
+ *
+ * Copyright (C) 2013 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "gtksourcesearch.h"
+#include "gtksourcebuffer.h"
+#include "gtksourcestylescheme.h"
+#include "gtktextregion.h"
+#include "gtksourcestyle-private.h"
+
+#include <string.h>
+
+/* Implementation overview:
+ *
+ * When the state of the search changes (the text to search or the flags), 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(1). We must just pay attention to contiguous matches.
+ *
+ * While the user is typing the text in the search entry, the buffer is scanned
+ * to count the number of occurrences. And when the user wants to do an
+ * operation (go to the next occurrence for example), chances are that the
+ * buffer has already been scanned entirely, so almost all the operations will
+ * be really fast.
+ *
+ * Extreme example:
+ * <occurrence> [1 GB of text] <next-occurrence>
+ * Once the buffer is scanned, switching between the occurrences will be almost
+ * instantaneous.
+ *
+ * So how to count the number of occurrences then? Remember that the buffer
+ * contents can be modified during the scan, and that we keep the old
+ * found_tag's. Moreover, when we encounter an old found_tag region, in the
+ * general case we can not say how many occurrences there are in this region,
+ * since a found_tag region can contain contiguous matches. Take for example the
+ * found_tag region "aa": was it the "aa" search match, or two times "a"?
+ * The implemented solution is to set occurrences_count to 0 when the search
+ * state changes, even if old matches are still there. Because it is not
+ * possible to count the old matches to decrement occurrences_count (and storing
+ * the previous search text would not be sufficient, because even older matches
+ * can still be there). To increment and decrement occurrences_count, there is
+ * the scan_region, the region to scan. If an occurrence is contained in
+ * scan_region, it means that it has not already been scanned, so
+ * occurrences_count doesn't take into account this occurrence. On the other
+ * hand, if we find an occurrence outside scan_region, the occurrence is
+ * normally correctly highlighted, and occurrences_count take it into account.
+ *
+ * So when we highlight or when we remove the highlight of an occurrence (on
+ * text insertion, deletion, when scanning, etc.), we increment or decrement
+ * occurrences_count depending on whether the occurrence was already taken into
+ * account by occurrences_count.
+ *
+ * If the code seems too complicated and contains strange bugs, you have two
+ * choices:
+ * - Write more unit tests, understand correctly the code and fix it.
+ * - Rewrite the code to implement the simpler solution explained above :-)
+ */
+
+/*
+#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. */
+#define SCAN_BATCH_SIZE 50
+
+struct _GtkSourceSearchPrivate
+{
+       GtkTextBuffer *buffer;
+
+       /* State of the search. If text is NULL, the search is disabled. */
+       gchar *text;
+       gint text_nb_lines;
+       GtkTextSearchFlags flags;
+
+       /* The region to scan and highlight. If NULL, the scan is finished. */
+       GtkTextRegion *scan_region;
+
+       /* The region to scan and highlight in priority. I.e. the visible part
+        * of the buffer on the screen.
+        */
+       GtkTextRegion *high_priority_region;
+
+       gulong idle_scan_id;
+
+       guint occurrences_count;
+
+       GtkTextTag *found_tag;
+};
+
+#define GTK_SOURCE_SEARCH_GET_PRIVATE(object) \
+        (G_TYPE_INSTANCE_GET_PRIVATE ((object), \
+                                      GTK_SOURCE_TYPE_SEARCH, \
+                                      GtkSourceSearchPrivate))
+
+G_DEFINE_TYPE (GtkSourceSearch, _gtk_source_search, G_TYPE_OBJECT);
+
+static gboolean
+dispose_has_run (GtkSourceSearch *search)
+{
+       return search->priv->buffer == NULL;
+}
+
+static void
+sync_found_tag (GtkSourceSearch *search)
+{
+       GtkSourceStyleScheme *style_scheme;
+       GtkSourceStyle *style = NULL;
+
+       if (dispose_has_run (search))
+       {
+               return;
+       }
+
+       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 ("search-match style not available.");
+       }
+
+       _gtk_source_style_apply (style, search->priv->found_tag);
+}
+
+/* Make sure to call this function when the buffer is constructed, else there is
+ * a problem with the tag table already initialized while it shouldn't.
+ */
+static void
+init_found_tag (GtkSourceSearch *search)
+{
+       search->priv->found_tag = gtk_text_buffer_create_tag (search->priv->buffer, NULL, NULL);
+
+       sync_found_tag (search);
+
+       g_signal_connect_object (search->priv->buffer,
+                                "notify::style-scheme",
+                                G_CALLBACK (sync_found_tag),
+                                search,
+                                G_CONNECT_SWAPPED);
+}
+
+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);
+}
+
+/* A TextRegion can contain empty subregions. So checking the number of
+ * subregions is not sufficient.
+ * When calling gtk_text_region_add() with equal iters, the subregion is not
+ * added. But when a subregion becomes empty, due to text deletion, the
+ * subregion is not removed from the TextRegion.
+ */
+static gboolean
+is_text_region_empty (GtkTextRegion *region)
+{
+       GtkTextRegionIterator region_iter;
+
+       if (region == NULL)
+       {
+               return TRUE;
+       }
+
+       gtk_text_region_get_iterator (region, &region_iter, 0);
+
+       while (!gtk_text_region_iterator_is_end (&region_iter))
+       {
+               GtkTextIter region_start;
+               GtkTextIter region_end;
+
+               gtk_text_region_iterator_get_subregion (&region_iter,
+                                                       &region_start,
+                                                       &region_end);
+
+               if (!gtk_text_iter_equal (&region_start, &region_end))
+               {
+                       return FALSE;
+               }
+
+               gtk_text_region_iterator_next (&region_iter);
+       }
+
+       return TRUE;
+}
+
+/* Sets @start and @end to the first non-empty subregion.
+ * Returns FALSE if the region is empty.
+ */
+static gboolean
+get_first_subregion (GtkTextRegion *region,
+                    GtkTextIter   *start,
+                    GtkTextIter   *end)
+{
+       GtkTextRegionIterator region_iter;
+
+       if (region == NULL)
+       {
+               return FALSE;
+       }
+
+       gtk_text_region_get_iterator (region, &region_iter, 0);
+
+       while (!gtk_text_region_iterator_is_end (&region_iter))
+       {
+               gtk_text_region_iterator_get_subregion (&region_iter, start, end);
+
+               if (!gtk_text_iter_equal (start, end))
+               {
+                       return TRUE;
+               }
+
+               gtk_text_region_iterator_next (&region_iter);
+       }
+
+       return FALSE;
+}
+
+static void
+clear_search (GtkSourceSearch *search)
+{
+       if (search->priv->scan_region != NULL)
+       {
+               gtk_text_region_destroy (search->priv->scan_region, TRUE);
+               search->priv->scan_region = NULL;
+       }
+
+       if (search->priv->high_priority_region != NULL)
+       {
+               gtk_text_region_destroy (search->priv->high_priority_region, TRUE);
+               search->priv->high_priority_region = NULL;
+       }
+
+       if (search->priv->idle_scan_id != 0)
+       {
+               g_source_remove (search->priv->idle_scan_id);
+               search->priv->idle_scan_id = 0;
+       }
+
+       search->priv->occurrences_count = 0;
+}
+
+/* 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 (GtkSourceSearch *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 (is_text_region_empty (search->priv->scan_region))
+               {
+                       /* 'start' is in a correct match, we can skip it. */
+
+                       if (!gtk_text_iter_ends_tag (start, search->priv->found_tag))
+                       {
+                               gtk_text_iter_forward_to_tag_toggle (start, search->priv->found_tag);
+                       }
+               }
+               else
+               {
+                       GtkTextIter tag_start = *start;
+                       GtkTextIter tag_end = *start;
+                       GtkTextRegion *region;
+
+                       if (!gtk_text_iter_begins_tag (&tag_start, search->priv->found_tag))
+                       {
+                               gtk_text_iter_backward_to_tag_toggle (&tag_start, search->priv->found_tag);
+                       }
+
+                       if (!gtk_text_iter_ends_tag (&tag_end, search->priv->found_tag))
+                       {
+                               gtk_text_iter_forward_to_tag_toggle (&tag_end, search->priv->found_tag);
+                       }
+
+                       region = gtk_text_region_intersect (search->priv->scan_region,
+                                                           &tag_start,
+                                                           &tag_end);
+
+                       if (is_text_region_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;
+                       }
+
+                       if (region != NULL)
+                       {
+                               gtk_text_region_destroy (region, TRUE);
+                       }
+               }
+       }
+
+       /* Symmetric for 'end'. */
+
+       if (gtk_text_iter_has_tag (end, search->priv->found_tag))
+       {
+               if (is_text_region_empty (search->priv->scan_region))
+               {
+                       /* 'end' is in a correct match, we can skip it. */
+
+                       if (!gtk_text_iter_begins_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;
+                       GtkTextRegion *region;
+
+                       if (!gtk_text_iter_begins_tag (&tag_start, search->priv->found_tag))
+                       {
+                               gtk_text_iter_backward_to_tag_toggle (&tag_start, search->priv->found_tag);
+                       }
+
+                       if (!gtk_text_iter_ends_tag (&tag_end, search->priv->found_tag))
+                       {
+                               gtk_text_iter_forward_to_tag_toggle (&tag_end, search->priv->found_tag);
+                       }
+
+                       region = gtk_text_region_intersect (search->priv->scan_region,
+                                                           &tag_start,
+                                                           &tag_end);
+
+                       if (is_text_region_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;
+                       }
+
+                       if (region != NULL)
+                       {
+                               gtk_text_region_destroy (region, TRUE);
+                       }
+               }
+       }
+
+       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));
+       });
+}
+
+/* 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 (GtkSourceSearch *search,
+                            GtkTextIter     *start,
+                            GtkTextIter     *end)
+{
+       GtkTextIter iter;
+       GtkTextIter match_start;
+       GtkTextIter match_end;
+
+       if (search->priv->found_tag == NULL)
+       {
+               init_found_tag (search);
+       }
+
+       if (gtk_text_iter_has_tag (start, search->priv->found_tag) &&
+           !gtk_text_iter_toggles_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_toggles_tag (end, search->priv->found_tag))
+       {
+               gtk_text_iter_forward_to_tag_toggle (end, search->priv->found_tag);
+       }
+
+       gtk_text_buffer_remove_tag (search->priv->buffer,
+                                   search->priv->found_tag,
+                                   start,
+                                   end);
+
+       if (search->priv->text == NULL)
+       {
+               return;
+       }
+
+       iter = *start;
+
+       /* TODO optimization: search with forward_to_tag_toggle() */
+       while (gtk_text_iter_forward_search (&iter,
+                                            search->priv->text,
+                                            search->priv->flags,
+                                            &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
+               {
+                       GtkTextRegion *region = gtk_text_region_intersect (search->priv->scan_region,
+                                                                          &match_start,
+                                                                          &match_end);
+
+                       if (is_text_region_empty (region))
+                       {
+                               search->priv->occurrences_count--;
+                       }
+
+                       if (region != NULL)
+                       {
+                               gtk_text_region_destroy (region, TRUE);
+                       }
+               }
+
+               iter = match_end;
+       }
+}
+
+static void
+scan_subregion (GtkSourceSearch *search,
+               GtkTextIter     *start,
+               GtkTextIter     *end)
+{
+       GtkTextIter iter;
+       GtkTextIter *limit;
+       gboolean found = TRUE;
+
+       if (search->priv->found_tag == NULL)
+       {
+               init_found_tag (search);
+       }
+
+       /* 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");
+                       gtk_text_region_debug_print (search->priv->scan_region);
+               });
+
+               gtk_text_region_subtract (search->priv->scan_region, start, end);
+
+               DEBUG ({
+                       g_print ("Region to scan, after:\n");
+                       gtk_text_region_debug_print (search->priv->scan_region);
+               });
+       }
+
+       if (search->priv->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 = gtk_text_iter_forward_search (&iter,
+                                                     search->priv->text,
+                                                     search->priv->flags,
+                                                     &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 (GtkSourceSearch *search,
+                GtkTextRegion   *region_to_highlight)
+{
+       gint nb_subregions = gtk_text_region_subregions (region_to_highlight);
+       GtkTextIter start_search;
+       GtkTextIter end_search;
+
+       if (nb_subregions == 0)
+       {
+               return;
+       }
+
+       gtk_text_region_nth_subregion (region_to_highlight,
+                                      0,
+                                      &start_search,
+                                      NULL);
+
+       gtk_text_region_nth_subregion (region_to_highlight,
+                                      nb_subregions - 1,
+                                      NULL,
+                                      &end_search);
+
+       gtk_text_iter_order (&start_search, &end_search);
+
+       scan_subregion (search, &start_search, &end_search);
+}
+
+/* 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.
+ */
+static void
+scan_chunk_region (GtkSourceSearch *search)
+{
+       gint nb_remaining_lines = SCAN_BATCH_SIZE;
+       GtkTextIter start;
+       GtkTextIter end;
+
+       while (nb_remaining_lines > 0 &&
+              get_first_subregion (search->priv->scan_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);
+
+               start_line = gtk_text_iter_get_line (&start);
+               limit_line = gtk_text_iter_get_line (&limit);
+
+               nb_remaining_lines -= limit_line - start_line;
+       }
+}
+
+static gboolean
+idle_scan_cb (GtkSourceSearch *search)
+{
+       gboolean finished = FALSE;
+
+       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);
+
+               gtk_text_region_destroy (search->priv->high_priority_region, TRUE);
+               search->priv->high_priority_region = NULL;
+       }
+       else
+       {
+               scan_chunk_region (search);
+       }
+
+       if (is_text_region_empty (search->priv->scan_region))
+       {
+               finished = TRUE;
+               search->priv->idle_scan_id = 0;
+
+               g_object_notify (G_OBJECT (search->priv->buffer), "search-occurrences-count");
+
+               if (search->priv->scan_region != NULL)
+               {
+                       gtk_text_region_destroy (search->priv->scan_region, TRUE);
+                       search->priv->scan_region = NULL;
+               }
+       }
+
+       return !finished;
+}
+
+static void
+install_idle_scan (GtkSourceSearch *search)
+{
+       if (search->priv->idle_scan_id == 0)
+       {
+               search->priv->idle_scan_id = g_idle_add ((GSourceFunc)idle_scan_cb, search);
+       }
+}
+
+static void
+add_subregion_to_scan (GtkSourceSearch   *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_text_region_new (search->priv->buffer);
+       }
+
+       DEBUG ({
+               g_print ("add_subregion_to_scan(): region to scan, before:\n");
+               gtk_text_region_debug_print (search->priv->scan_region);
+       });
+
+       gtk_text_region_add (search->priv->scan_region, &start, &end);
+
+       DEBUG ({
+               g_print ("add_subregion_to_scan(): region to scan, after:\n");
+               gtk_text_region_debug_print (search->priv->scan_region);
+       });
+
+       install_idle_scan (search);
+
+       /* The highlighting can be modified a bit backward and forward the
+        * region.
+        */
+       gtk_text_iter_backward_lines (&start, search->priv->text_nb_lines);
+       gtk_text_iter_forward_lines (&end, search->priv->text_nb_lines);
+
+       g_signal_emit_by_name (search->priv->buffer, "highlight-updated", &start, &end);
+}
+
+static void
+update (GtkSourceSearch *search)
+{
+       GtkTextIter start;
+       GtkTextIter end;
+
+       if (dispose_has_run (search))
+       {
+               return;
+       }
+
+       clear_search (search);
+
+       search->priv->scan_region = gtk_text_region_new (search->priv->buffer);
+
+       gtk_text_buffer_get_bounds (search->priv->buffer, &start, &end);
+       add_subregion_to_scan (search, &start, &end);
+}
+
+static void
+insert_text_before_cb (GtkSourceSearch *search,
+                      GtkTextIter     *location,
+                      gchar           *text,
+                      gint             length)
+{
+       if (search->priv->text != NULL)
+       {
+               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 (GtkSourceSearch *search,
+                     GtkTextIter     *location,
+                     gchar           *text,
+                     gint             length)
+{
+       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 (GtkSourceSearch *search,
+                       GtkTextIter     *delete_start,
+                       GtkTextIter     *delete_end)
+{
+       GtkTextIter start_buffer;
+       GtkTextIter end_buffer;
+
+       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->priv->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 (GtkSourceSearch *search,
+                      GtkTextIter     *start,
+                      GtkTextIter     *end)
+{
+       add_subregion_to_scan (search, start, end);
+}
+
+static void
+set_buffer (GtkSourceSearch *search,
+           GtkSourceBuffer *buffer)
+{
+       g_assert (search->priv->buffer == NULL);
+
+       search->priv->buffer = GTK_TEXT_BUFFER (buffer);
+       g_object_ref (buffer);
+
+       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);
+}
+
+static void
+_gtk_source_search_dispose (GObject *object)
+{
+       GtkSourceSearch *search = GTK_SOURCE_SEARCH (object);
+
+       clear_search (search);
+       g_clear_object (&search->priv->buffer);
+
+       G_OBJECT_CLASS (_gtk_source_search_parent_class)->dispose (object);
+}
+
+static void
+_gtk_source_search_finalize (GObject *object)
+{
+       GtkSourceSearch *search = GTK_SOURCE_SEARCH (object);
+
+       g_free (search->priv->text);
+
+       G_OBJECT_CLASS (_gtk_source_search_parent_class)->finalize (object);
+}
+
+static void
+_gtk_source_search_class_init (GtkSourceSearchClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->dispose = _gtk_source_search_dispose;
+       object_class->finalize = _gtk_source_search_finalize;
+
+       g_type_class_add_private (object_class, sizeof (GtkSourceSearchPrivate));
+}
+
+static void
+_gtk_source_search_init (GtkSourceSearch *self)
+{
+       self->priv = GTK_SOURCE_SEARCH_GET_PRIVATE (self);
+}
+
+GtkSourceSearch *
+_gtk_source_search_new (GtkSourceBuffer *buffer)
+{
+       GtkSourceSearch *search;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), NULL);
+
+       search = g_object_new (GTK_SOURCE_TYPE_SEARCH, NULL);
+       set_buffer (search, buffer);
+
+       return search;
+}
+
+static gint
+compute_nb_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;
+}
+
+void
+_gtk_source_search_set_text (GtkSourceSearch *search,
+                            const gchar     *text)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SEARCH (search));
+       g_return_if_fail (text == NULL || g_utf8_validate (text, -1, NULL));
+
+       g_free (search->priv->text);
+
+       if (text == NULL || *text == '\0')
+       {
+               search->priv->text = NULL;
+       }
+       else
+       {
+               search->priv->text = g_strdup (text);
+       }
+
+       search->priv->text_nb_lines = compute_nb_of_lines (search->priv->text);
+
+       update (search);
+}
+
+const gchar *
+_gtk_source_search_get_text (GtkSourceSearch *search)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), NULL);
+
+       return search->priv->text;
+}
+
+void
+_gtk_source_search_set_flags (GtkSourceSearch      *search,
+                             GtkSourceSearchFlags  flags)
+{
+       g_return_if_fail (GTK_SOURCE_IS_SEARCH (search));
+
+       if (flags & GTK_SOURCE_SEARCH_CASE_SENSITIVE)
+       {
+               search->priv->flags &= ~GTK_TEXT_SEARCH_CASE_INSENSITIVE;
+       }
+       else
+       {
+               search->priv->flags |= GTK_TEXT_SEARCH_CASE_INSENSITIVE;
+       }
+
+       update (search);
+}
+
+GtkSourceSearchFlags
+_gtk_source_search_get_flags (GtkSourceSearch *search)
+{
+       GtkSourceSearchFlags source_flags = 0;
+
+       g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), 0);
+
+       if ((search->priv->flags & GTK_TEXT_SEARCH_CASE_INSENSITIVE) == 0)
+       {
+               source_flags |= GTK_SOURCE_SEARCH_CASE_SENSITIVE;
+       }
+
+       return source_flags;
+}
+
+void
+_gtk_source_search_update_highlight (GtkSourceSearch   *search,
+                                    const GtkTextIter *start,
+                                    const GtkTextIter *end,
+                                    gboolean           synchronous)
+{
+       GtkTextRegion *region_to_highlight;
+
+       g_return_if_fail (GTK_SOURCE_IS_SEARCH (search));
+       g_return_if_fail (start != NULL);
+       g_return_if_fail (end != NULL);
+
+       if (dispose_has_run (search) ||
+           is_text_region_empty (search->priv->scan_region))
+       {
+               return;
+       }
+
+       region_to_highlight = gtk_text_region_intersect (search->priv->scan_region,
+                                                        start,
+                                                        end);
+
+       if (is_text_region_empty (region_to_highlight))
+       {
+               if (region_to_highlight != NULL)
+               {
+                       gtk_text_region_destroy (region_to_highlight, TRUE);
+               }
+
+               return;
+       }
+
+       if (synchronous)
+       {
+               scan_all_region (search, region_to_highlight);
+               gtk_text_region_destroy (region_to_highlight, TRUE);
+       }
+       else
+       {
+               if (search->priv->high_priority_region != NULL)
+               {
+                       /* The high priority region is used to highlight the
+                        * region visible on screen. So if we are here, that
+                        * means that the visible region has changed. So we can
+                        * destroy the old high_priority_region.
+                        */
+                       gtk_text_region_destroy (search->priv->high_priority_region, TRUE);
+               }
+
+               search->priv->high_priority_region = region_to_highlight;
+               install_idle_scan (search);
+       }
+}
+
+guint
+_gtk_source_search_get_occurrences_count (GtkSourceSearch *search)
+{
+       g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), 0);
+
+       return search->priv->occurrences_count;
+}
diff --git a/gtksourceview/gtksourcesearch.h b/gtksourceview/gtksourcesearch.h
new file mode 100644
index 0000000..6fe023b
--- /dev/null
+++ b/gtksourceview/gtksourcesearch.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*-
+ * gtksourcesearch.h
+ * This file is part of GtkSourceView
+ *
+ * Copyright (C) 2013 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef __GTK_SOURCE_SEARCH_H__
+#define __GTK_SOURCE_SEARCH_H__
+
+#include <gtk/gtk.h>
+#include "gtksourcetypes.h"
+#include "gtksourcetypes-private.h"
+#include "gtksourcebuffer.h"
+
+G_BEGIN_DECLS
+
+#define GTK_SOURCE_TYPE_SEARCH             (_gtk_source_search_get_type ())
+#define GTK_SOURCE_SEARCH(obj)             (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_SOURCE_TYPE_SEARCH, 
GtkSourceSearch))
+#define GTK_SOURCE_SEARCH_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_SOURCE_TYPE_SEARCH, 
GtkSourceSearchClass))
+#define GTK_SOURCE_IS_SEARCH(obj)          (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_SOURCE_TYPE_SEARCH))
+#define GTK_SOURCE_IS_SEARCH_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_SOURCE_TYPE_SEARCH))
+#define GTK_SOURCE_SEARCH_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_SOURCE_TYPE_SEARCH, 
GtkSourceSearchClass))
+
+typedef struct _GtkSourceSearchClass    GtkSourceSearchClass;
+typedef struct _GtkSourceSearchPrivate  GtkSourceSearchPrivate;
+
+struct _GtkSourceSearch
+{
+       GObject parent;
+
+       GtkSourceSearchPrivate *priv;
+};
+
+struct _GtkSourceSearchClass
+{
+       GObjectClass parent_class;
+};
+
+G_GNUC_INTERNAL
+GType                  _gtk_source_search_get_type                     (void) G_GNUC_CONST;
+
+G_GNUC_INTERNAL
+GtkSourceSearch *      _gtk_source_search_new                          (GtkSourceBuffer        *buffer);
+
+G_GNUC_INTERNAL
+void                   _gtk_source_search_set_text                     (GtkSourceSearch        *search,
+                                                                        const gchar            *text);
+
+G_GNUC_INTERNAL
+const gchar *          _gtk_source_search_get_text                     (GtkSourceSearch        *search);
+
+G_GNUC_INTERNAL
+void                   _gtk_source_search_set_flags                    (GtkSourceSearch        *search,
+                                                                        GtkSourceSearchFlags    flags);
+
+G_GNUC_INTERNAL
+GtkSourceSearchFlags   _gtk_source_search_get_flags                    (GtkSourceSearch        *search);
+
+G_GNUC_INTERNAL
+void                   _gtk_source_search_update_highlight             (GtkSourceSearch        *search,
+                                                                        const GtkTextIter      *start,
+                                                                        const GtkTextIter      *end,
+                                                                        gboolean                synchronous);
+
+guint                  _gtk_source_search_get_occurrences_count        (GtkSourceSearch        *search);
+
+G_END_DECLS
+
+#endif /* __GTK_SOURCE_SEARCH_H__ */
diff --git a/gtksourceview/gtksourcetypes-private.h b/gtksourceview/gtksourcetypes-private.h
index 6b88c2c..58ccf31 100644
--- a/gtksourceview/gtksourcetypes-private.h
+++ b/gtksourceview/gtksourcetypes-private.h
@@ -34,6 +34,7 @@ typedef struct _GtkSourceGutterRendererLines  GtkSourceGutterRendererLines;
 typedef struct _GtkSourceGutterRendererMarks   GtkSourceGutterRendererMarks;
 typedef struct _GtkSourcePixbufHelper          GtkSourcePixbufHelper;
 typedef struct _GtkSourceRegex                 GtkSourceRegex;
+typedef struct _GtkSourceSearch                        GtkSourceSearch;
 typedef struct _GtkSourceUndoManagerDefault    GtkSourceUndoManagerDefault;
 
 G_END_DECLS
diff --git a/gtksourceview/gtksourceutils.c b/gtksourceview/gtksourceutils.c
new file mode 100644
index 0000000..3347dd1
--- /dev/null
+++ b/gtksourceview/gtksourceutils.c
@@ -0,0 +1,189 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */
+/* gtksourceutils.c
+ * This file is part of GtkSourceView
+ *
+ * Copyright (C) 2005 - Paolo Borelli
+ * Copyright (C) 2013 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+/**
+ * SECTION:utils
+ * @title: GtkSourceUtils
+ * @short_description: Utilities functions
+ *
+ * Utilities functions.
+ */
+
+#include "gtksourceutils.h"
+#include <string.h>
+
+/**
+ * gtk_source_utils_unescape_search_text:
+ * @text: the text to unescape.
+ *
+ * Use this function before gtk_source_buffer_set_search_text(), to unescape
+ * certain sequences of characters: \n, \r, \t and \\. The purpose is to easily
+ * write those characters in a search entry.
+ *
+ * See also: gtk_source_utils_escape_search_text().
+ *
+ * Since: 3.10
+ */
+gchar *
+gtk_source_utils_unescape_search_text (const gchar *text)
+{
+       GString *str;
+       gint length;
+       gboolean drop_prev = FALSE;
+       const gchar *cur;
+       const gchar *end;
+       const gchar *prev;
+
+       if (text == NULL)
+       {
+               return NULL;
+       }
+
+       length = strlen (text);
+
+       str = g_string_new ("");
+
+       cur = text;
+       end = text + length;
+       prev = NULL;
+
+       while (cur != end)
+       {
+               const gchar *next;
+               next = g_utf8_next_char (cur);
+
+               if (prev && (*prev == '\\'))
+               {
+                       switch (*cur)
+                       {
+                               case 'n':
+                                       str = g_string_append (str, "\n");
+                                       break;
+                               case 'r':
+                                       str = g_string_append (str, "\r");
+                                       break;
+                               case 't':
+                                       str = g_string_append (str, "\t");
+                                       break;
+                               case '\\':
+                                       str = g_string_append (str, "\\");
+                                       drop_prev = TRUE;
+                                       break;
+                               default:
+                                       str = g_string_append (str, "\\");
+                                       str = g_string_append_len (str, cur, next - cur);
+                                       break;
+                       }
+               }
+               else if (*cur != '\\')
+               {
+                       str = g_string_append_len (str, cur, next - cur);
+               }
+               else if ((next == end) && (*cur == '\\'))
+               {
+                       str = g_string_append (str, "\\");
+               }
+
+               if (!drop_prev)
+               {
+                       prev = cur;
+               }
+               else
+               {
+                       prev = NULL;
+                       drop_prev = FALSE;
+               }
+
+               cur = next;
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+/**
+ * gtk_source_utils_escape_search_text:
+ * @text: the text to escape.
+ *
+ * Use this function after gtk_source_buffer_get_search_text(), to escape
+ * certain characters: \n, \r, \t and \.
+ *
+ * See also: gtk_source_utils_unescape_search_text().
+ *
+ * Since: 3.10
+ */
+gchar *
+gtk_source_utils_escape_search_text (const gchar* text)
+{
+       GString *str;
+       gint length;
+       const gchar *p;
+       const gchar *end;
+
+       if (text == NULL)
+       {
+               return NULL;
+       }
+
+       length = strlen (text);
+
+       /* no escape when typing.
+        * The short circuit works only for ascii, but we only
+        * care about not escaping a single '\' */
+       if (length == 1)
+       {
+               return g_strdup (text);
+       }
+
+       str = g_string_new ("");
+
+       p = text;
+       end = text + length;
+
+       while (p != end)
+       {
+               const gchar *next;
+               next = g_utf8_next_char (p);
+
+               switch (*p)
+               {
+                       case '\n':
+                               g_string_append (str, "\\n");
+                               break;
+                       case '\r':
+                               g_string_append (str, "\\r");
+                               break;
+                       case '\t':
+                               g_string_append (str, "\\t");
+                               break;
+                       case '\\':
+                               g_string_append (str, "\\\\");
+                               break;
+                       default:
+                               g_string_append_len (str, p, next - p);
+                               break;
+               }
+
+               p = next;
+       }
+
+       return g_string_free (str, FALSE);
+}
diff --git a/gtksourceview/gtksourceutils.h b/gtksourceview/gtksourceutils.h
new file mode 100644
index 0000000..2a39ed9
--- /dev/null
+++ b/gtksourceview/gtksourceutils.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*-
+ * gtksourceutils.h
+ * This file is part of GtkSourceView
+ *
+ * Copyright (C) 2013 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef __GTK_SOURCE_UTILS_H__
+#define __GTK_SOURCE_UTILS_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+gchar          *gtk_source_utils_unescape_search_text          (const gchar    *text);
+gchar          *gtk_source_utils_escape_search_text            (const gchar    *text);
+
+G_END_DECLS
+
+#endif /* __GTK_SOURCE_UTILS_H__ */
diff --git a/po/POTFILES.in b/po/POTFILES.in
index aebf5cf..c20b581 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -131,9 +131,11 @@ gtksourceview/gtksourcemarkattributes.c
 gtksourceview/gtksourcemark.c
 gtksourceview/gtksourceprintcompositor.c
 gtksourceview/gtksourceregex.c
+gtksourceview/gtksourcesearch.c
 gtksourceview/gtksourcestyle.c
 gtksourceview/gtksourcestylescheme.c
 gtksourceview/gtksourcestyleschememanager.c
 gtksourceview/gtksourceundomanagerdefault.c
+gtksourceview/gtksourceutils.c
 gtksourceview/gtksourceview.c
 gtksourceview/gtksourceview-i18n.c
diff --git a/tests/Makefile.am b/tests/Makefile.am
index ef30933..7a55e7d 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -13,12 +13,16 @@ AM_CPPFLAGS =                               \
 noinst_PROGRAMS = $(TEST_PROGS) $(UNIT_TEST_PROGS)
 TESTS = $(UNIT_TEST_PROGS)
 
-BUILT_SOURCES = \
-       test-completion-resources.c
+BUILT_SOURCES =                                \
+       test-completion-resources.c     \
+       test-search-ui-resources.c
 
 test-completion-resources.c: test-completion.gresource.xml $(shell $(GLIB_COMPILE_RESOURCES) 
--generate-dependencies $(srcdir)/test-completion.gresource.xml)
        $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(srcdir) --generate-source 
$(srcdir)/test-completion.gresource.xml
 
+test-search-ui-resources.c: test-search-ui.gresource.xml $(shell $(GLIB_COMPILE_RESOURCES) 
--generate-dependencies $(srcdir)/test-search-ui.gresource.xml)
+       $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(srcdir) --generate-source 
$(srcdir)/test-search-ui.gresource.xml
+
 TEST_PROGS = test-widget
 test_widget_SOURCES = test-widget.c
 test_widget_LDADD =                    \
@@ -27,14 +31,23 @@ test_widget_LDADD =                         \
        $(TESTS_LIBS)
 
 TEST_PROGS += test-completion
-test_completion_SOURCES =      \
-       $(BUILT_SOURCES)        \
-       test-completion.c
+test_completion_SOURCES =              \
+       test-completion.c               \
+       test-completion-resources.c
 test_completion_LDADD =                \
        $(top_builddir)/gtksourceview/libgtksourceview-3.0.la \
        $(DEP_LIBS)                     \
        $(TESTS_LIBS)
 
+TEST_PROGS += test-search-ui
+test_search_ui_SOURCES =               \
+       test-search-ui.c                \
+       test-search-ui-resources.c
+test_search_ui_LDADD =                                         \
+       $(top_builddir)/gtksourceview/libgtksourceview-3.0.la   \
+       $(DEP_LIBS)                                             \
+       $(TESTS_LIBS)
+
 UNIT_TEST_PROGS = test-languagemanager
 test_languagemanager_SOURCES =         \
        test-languagemanager.c
@@ -119,6 +132,14 @@ test_undo_manager_LDADD =                                  \
        $(DEP_LIBS)                                             \
        $(TESTS_LIBS)
 
+UNIT_TEST_PROGS += test-search
+test_search_SOURCES = test-search.c
+test_search_LDADD =                                                    \
+       $(top_builddir)/gtksourceview/libgtksourceview-3.0.la           \
+       $(top_builddir)/gtksourceview/libgtksourceview-private.la       \
+       $(DEP_LIBS)                                                     \
+       $(TESTS_LIBS)
+
 python_tests =                 \
        test-completion.py      \
        test-widget.py
@@ -129,6 +150,8 @@ EXTRA_DIST =                                \
        styles/classic.xml              \
        test-completion.gresource.xml   \
        test-completion.ui              \
+       test-search-ui.gresource.xml    \
+       test-search-ui.ui               \
        $(python_tests)
 
 -include $(top_srcdir)/git.mk
diff --git a/tests/test-search-ui.c b/tests/test-search-ui.c
new file mode 100644
index 0000000..97d2272
--- /dev/null
+++ b/tests/test-search-ui.c
@@ -0,0 +1,169 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*-
+ * test-search-ui.c
+ * This file is part of GtkSourceView
+ *
+ * Copyright (C) 2013 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include <gtk/gtk.h>
+#include <gtksourceview/gtksource.h>
+
+static void
+open_file (GtkSourceBuffer *buffer,
+          const gchar     *filename)
+{
+       gchar *contents;
+       GError *error = NULL;
+       GtkSourceLanguageManager *language_manager;
+       GtkSourceLanguage *language;
+
+       if (!g_file_get_contents (filename, &contents, NULL, &error))
+       {
+               g_error ("Impossible to load file: %s", error->message);
+       }
+
+       gtk_text_buffer_set_text (GTK_TEXT_BUFFER (buffer), contents, -1);
+
+       language_manager = gtk_source_language_manager_get_default ();
+       language = gtk_source_language_manager_get_language (language_manager, "c");
+       gtk_source_buffer_set_language (buffer, language);
+
+       g_free (contents);
+}
+
+static void
+on_occurrences_count_notify_cb (GtkSourceBuffer *buffer,
+                               GParamSpec      *spec,
+                               GtkLabel        *label)
+{
+       guint occurrences_count = gtk_source_buffer_get_search_occurrences_count (buffer);
+       gchar *text = g_strdup_printf ("%u occurrences", occurrences_count);
+
+       gtk_label_set_text (label, text);
+       g_free (text);
+}
+
+static void
+on_search_entry_text_notify_cb (GtkEntry        *entry,
+                               GParamSpec      *spec,
+                               GtkSourceBuffer *buffer)
+{
+       const gchar *text = gtk_entry_get_text (entry);
+       gchar *unescaped_text = gtk_source_utils_unescape_search_text (text);
+
+       gtk_source_buffer_set_search_text (buffer, unescaped_text);
+       g_free (unescaped_text);
+}
+
+static void
+on_match_case_toggled_cb (GtkToggleButton *button,
+                         GtkSourceBuffer *buffer)
+{
+       GtkSourceSearchFlags flags = gtk_source_buffer_get_search_flags (buffer);
+
+       if (gtk_toggle_button_get_active (button))
+       {
+               flags |= GTK_SOURCE_SEARCH_CASE_SENSITIVE;
+       }
+       else
+       {
+               flags &= ~GTK_SOURCE_SEARCH_CASE_SENSITIVE;
+       }
+
+       gtk_source_buffer_set_search_flags (buffer, flags);
+}
+
+static void
+create_window (void)
+{
+       GtkBuilder *builder;
+       GError *error = NULL;
+       GtkWindow *window;
+       GtkSourceView *source_view;
+       GtkSourceBuffer *source_buffer;
+       GtkSearchEntry *search_entry;
+       GtkLabel *label_occurrences_count;
+       GtkCheckButton *match_case;
+       PangoFontDescription *font;
+
+       builder = gtk_builder_new ();
+
+       gtk_builder_add_from_resource (builder,
+                                      "/org/gnome/gtksourceview/tests/ui/test-search-ui.ui",
+                                      &error);
+
+       if (error != NULL)
+       {
+               g_error ("Impossible to load test-search-ui.ui: %s", error->message);
+       }
+
+       window = GTK_WINDOW (gtk_builder_get_object (builder, "window"));
+       source_view = GTK_SOURCE_VIEW (gtk_builder_get_object (builder, "source_view"));
+       search_entry = GTK_SEARCH_ENTRY (gtk_builder_get_object (builder, "search_entry"));
+       label_occurrences_count = GTK_LABEL (gtk_builder_get_object (builder, "label_occurrences_count"));
+       match_case = GTK_CHECK_BUTTON (gtk_builder_get_object (builder, "checkbutton_match_case"));
+
+       font = pango_font_description_from_string ("Monospace 10");
+       gtk_widget_override_font (GTK_WIDGET (source_view), font);
+
+       gtk_source_view_set_tab_width (source_view, 8);
+
+       /* Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=643732:
+        * "Source view is created with a GtkTextBuffer instead of GtkSourceBuffer"
+        */
+       source_buffer = gtk_source_buffer_new (NULL);
+
+       gtk_text_view_set_buffer (GTK_TEXT_VIEW (source_view),
+                                 GTK_TEXT_BUFFER (source_buffer));
+
+       g_object_unref (source_buffer);
+
+       open_file (source_buffer, TOP_SRCDIR "/gtksourceview/gtksourcesearch.c");
+
+       g_signal_connect (window,
+                         "destroy",
+                         G_CALLBACK (gtk_main_quit),
+                         NULL);
+
+       g_signal_connect (search_entry,
+                         "notify::text",
+                         G_CALLBACK (on_search_entry_text_notify_cb),
+                         source_buffer);
+
+       g_signal_connect (source_buffer,
+                         "notify::search-occurrences-count",
+                         G_CALLBACK (on_occurrences_count_notify_cb),
+                         label_occurrences_count);
+
+       g_signal_connect (match_case,
+                         "toggled",
+                         G_CALLBACK (on_match_case_toggled_cb),
+                         source_buffer);
+
+       g_object_unref (builder);
+}
+
+int
+main (int argc, char *argv[])
+{
+       gtk_init (&argc, &argv);
+
+       create_window ();
+
+       gtk_main ();
+       return 0;
+}
diff --git a/tests/test-search-ui.gresource.xml b/tests/test-search-ui.gresource.xml
new file mode 100644
index 0000000..1577bad
--- /dev/null
+++ b/tests/test-search-ui.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/gtksourceview/tests/ui">
+    <file preprocess="xml-stripblanks">test-search-ui.ui</file>
+  </gresource>
+</gresources>
diff --git a/tests/test-search-ui.ui b/tests/test-search-ui.ui
new file mode 100644
index 0000000..0a43de8
--- /dev/null
+++ b/tests/test-search-ui.ui
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.6 -->
+  <!-- interface-requires gtksourceview 3.0 -->
+  <object class="GtkWindow" id="window">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="default_width">700</property>
+    <property name="default_height">500</property>
+    <child>
+      <object class="GtkGrid" id="grid1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="border_width">6</property>
+        <property name="row_spacing">6</property>
+        <child>
+          <object class="GtkScrolledWindow" id="scrolledwindow1">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="shadow_type">in</property>
+            <child>
+              <object class="GtkSourceView" id="source_view">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="has_tooltip">True</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="left_margin">2</property>
+                <property name="right_margin">2</property>
+                <property name="tab_width">4</property>
+                <property name="auto_indent">True</property>
+                <property name="indent_on_tab">False</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkGrid" id="grid2">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="column_spacing">6</property>
+            <child>
+              <object class="GtkLabel" id="label1">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">0</property>
+                <property name="label">Search:</property>
+                <attributes>
+                  <attribute name="weight" value="bold"/>
+                </attributes>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkSearchEntry" id="search_entry">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="primary_icon_name">edit-find-symbolic</property>
+                <property name="primary_icon_activatable">False</property>
+                <property name="primary_icon_sensitive">False</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label_occurrences_count">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label">0 occurrences</property>
+              </object>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkGrid" id="grid3">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkCheckButton" id="checkbutton_match_case">
+                <property name="label">Match case</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+                <property name="xalign">0</property>
+                <property name="active">True</property>
+                <property name="draw_indicator">True</property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">2</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/tests/test-search.c b/tests/test-search.c
new file mode 100644
index 0000000..fe25417
--- /dev/null
+++ b/tests/test-search.c
@@ -0,0 +1,237 @@
+/*
+ * test-search.c
+ * This file is part of GtkSourceView
+ *
+ * Copyright (C) 2013 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include <gtk/gtk.h>
+#include <gtksourceview/gtksource.h>
+#include "gtksourceview/gtksourcesearch.h"
+
+static void
+flush_queue (void)
+{
+       while (gtk_events_pending ())
+       {
+               gtk_main_iteration ();
+       }
+}
+
+/* Without insertion or deletion of text in the buffer afterwards. */
+static void
+test_occurrences_count_simple (void)
+{
+       GtkSourceBuffer *buffer = gtk_source_buffer_new (NULL);
+       GtkTextIter iter;
+       guint occurrences_count;
+
+       gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &iter);
+       gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, "Some foo\nSome bar\n", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (buffer);
+       g_assert_cmpuint (occurrences_count, ==, 0);
+
+       gtk_source_buffer_set_search_text (buffer, "world");
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (buffer);
+       g_assert_cmpuint (occurrences_count, ==, 0);
+
+       gtk_source_buffer_set_search_text (buffer, "Some");
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (buffer);
+       g_assert_cmpuint (occurrences_count, ==, 2);
+
+       gtk_source_buffer_set_search_text (buffer, "foo");
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       gtk_source_buffer_set_search_text (buffer, "world");
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (buffer);
+       g_assert_cmpuint (occurrences_count, ==, 0);
+
+       g_object_unref (buffer);
+}
+
+static void
+test_occurrences_count_with_insert (void)
+{
+       GtkSourceBuffer *source_buffer = gtk_source_buffer_new (NULL);
+       GtkTextBuffer *text_buffer = GTK_TEXT_BUFFER (source_buffer);
+       GtkTextIter iter;
+       guint occurrences_count;
+
+       /* Contents: "foobar" */
+       gtk_text_buffer_get_start_iter (text_buffer, &iter);
+       gtk_text_buffer_insert (text_buffer, &iter, "foobar", -1);
+
+       gtk_source_buffer_set_search_text (source_buffer, "foo");
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       /* Contents: "foobar " */
+       gtk_text_buffer_get_end_iter (text_buffer, &iter);
+       gtk_text_buffer_insert (text_buffer, &iter, " ", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       /* Contents: "foobar foobeer" */
+       gtk_text_buffer_get_end_iter (text_buffer, &iter);
+       gtk_text_buffer_insert (text_buffer, &iter, "foobeer", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 2);
+
+       /* Contents: "foo bar foobeer" */
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &iter, 3);
+       gtk_text_buffer_insert (text_buffer, &iter, " ", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 2);
+
+       /* Contents: "foto bar foobeer" */
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &iter, 2);
+       gtk_text_buffer_insert (text_buffer, &iter, "t", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       /* Contents: "footo bar foobeer" */
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &iter, 2);
+       gtk_text_buffer_insert (text_buffer, &iter, "o", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 2);
+
+       /* Contents: "foofooto bar foobeer" */
+       gtk_text_buffer_get_start_iter (text_buffer, &iter);
+       gtk_text_buffer_insert (text_buffer, &iter, "foo", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 3);
+
+       /* Contents: "fooTfooto bar foobeer" */
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &iter, 3);
+       gtk_text_buffer_insert (text_buffer, &iter, "T", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 3);
+
+       g_object_unref (source_buffer);
+}
+
+static void
+test_occurrences_count_with_delete (void)
+{
+       GtkSourceBuffer *source_buffer = gtk_source_buffer_new (NULL);
+       GtkTextBuffer *text_buffer = GTK_TEXT_BUFFER (source_buffer);
+       GtkTextIter start;
+       GtkTextIter end;
+       guint occurrences_count;
+
+       gtk_source_buffer_set_search_text (source_buffer, "foo");
+
+       /* Contents: "foo" -> "" */
+       gtk_text_buffer_set_text (text_buffer, "foo", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       gtk_text_buffer_get_bounds (text_buffer, &start, &end);
+       gtk_text_buffer_delete (text_buffer, &start, &end);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 0);
+
+       /* Contents: "foo" -> "oo" */
+       gtk_text_buffer_set_text (text_buffer, "foo", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       gtk_text_buffer_get_start_iter (text_buffer, &start);
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &end, 1);
+       gtk_text_buffer_delete (text_buffer, &start, &end);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 0);
+
+       /* Contents: "foobar foobeer" -> "foobar" */
+       gtk_text_buffer_set_text (text_buffer, "foobar foobeer", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 2);
+
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &start, 6);
+       gtk_text_buffer_get_end_iter (text_buffer, &end);
+       gtk_text_buffer_delete (text_buffer, &start, &end);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       /* Contents: "foo[foo]foo" -> "foofoo" */
+       gtk_text_buffer_set_text (text_buffer, "foofoofoo", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 3);
+
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &start, 3);
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &end, 6);
+       gtk_text_buffer_delete (text_buffer, &start, &end);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 2);
+
+       /* Contents: "fo[of]oo" -> "fooo" */
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &start, 2);
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &end, 4);
+       gtk_text_buffer_delete (text_buffer, &start, &end);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       /* Contents: "foto" -> "foo" */
+       gtk_text_buffer_set_text (text_buffer, "foto", -1);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 0);
+
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &start, 2);
+       gtk_text_buffer_get_iter_at_offset (text_buffer, &end, 3);
+       gtk_text_buffer_delete (text_buffer, &start, &end);
+       flush_queue ();
+       occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+       g_assert_cmpuint (occurrences_count, ==, 1);
+
+       g_object_unref (source_buffer);
+}
+
+int
+main (int argc, char **argv)
+{
+       gtk_test_init (&argc, &argv);
+
+       g_test_add_func ("/Search/occurrences-count/simple", test_occurrences_count_simple);
+       g_test_add_func ("/Search/occurrences-count/with-insert", test_occurrences_count_with_insert);
+       g_test_add_func ("/Search/occurrences-count/with-delete", test_occurrences_count_with_delete);
+
+       return g_test_run ();
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]