[gtksourceview/wip/search: 2/36] Higher-level asynchronous search and replace API
- From: Sébastien Wilmet <swilmet src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gtksourceview/wip/search: 2/36] Higher-level asynchronous search and replace API
- Date: Sat, 6 Jul 2013 15:56:54 +0000 (UTC)
commit 1f386620cb846536204ca2fc5b49a6f7a8364e32
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, ®ion_iter, 0);
+
+ while (!gtk_text_region_iterator_is_end (®ion_iter))
+ {
+ GtkTextIter region_start;
+ GtkTextIter region_end;
+
+ gtk_text_region_iterator_get_subregion (®ion_iter,
+ ®ion_start,
+ ®ion_end);
+
+ if (!gtk_text_iter_equal (®ion_start, ®ion_end))
+ {
+ return FALSE;
+ }
+
+ gtk_text_region_iterator_next (®ion_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, ®ion_iter, 0);
+
+ while (!gtk_text_region_iterator_is_end (®ion_iter))
+ {
+ gtk_text_region_iterator_get_subregion (®ion_iter, start, end);
+
+ if (!gtk_text_iter_equal (start, end))
+ {
+ return TRUE;
+ }
+
+ gtk_text_region_iterator_next (®ion_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]