[gtksourceview/wip/search: 4/5] 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: 4/5] Higher-level asynchronous search and replace API
- Date: Sun, 7 Jul 2013 12:06:47 +0000 (UTC)
commit 4423feff5acade12ce4f3521ca220951cd75b239
Author: Sébastien Wilmet <swilmet gnome org>
Date: Sat Jun 15 10:46:39 2013 +0200
Higher-level asynchronous search and replace API
And the implementation, the documentation, some unit tests, a test for
the performances, a mini program with a basic UI, and utilities
functions.
docs/reference/Makefile.am | 1 +
docs/reference/gtksourceview-3.0-sections.txt | 43 +-
docs/reference/gtksourceview-docs.xml | 1 +
gtksourceview/Makefile.am | 4 +
gtksourceview/gtksource.h | 1 +
gtksourceview/gtksourcebuffer.c | 717 ++++++++-
gtksourceview/gtksourcebuffer.h | 72 +
gtksourceview/gtksourcesearch.c | 2125 +++++++++++++++++++++++++
gtksourceview/gtksourcesearch.h | 156 ++
gtksourceview/gtksourcetypes-private.h | 1 +
gtksourceview/gtksourceutils.c | 191 +++
gtksourceview/gtksourceutils.h | 34 +
po/POTFILES.in | 2 +
tests/Makefile.am | 41 +-
tests/test-search-performances.c | 183 +++
tests/test-search-ui.c | 441 +++++
tests/test-search-ui.gresource.xml | 6 +
tests/test-search-ui.ui | 289 ++++
tests/test-search.c | 265 +++
19 files changed, 4532 insertions(+), 41 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..a432ef3 100644
--- a/docs/reference/gtksourceview-3.0-sections.txt
+++ b/docs/reference/gtksourceview-3.0-sections.txt
@@ -15,14 +15,6 @@ gtk_source_buffer_set_highlight_matching_brackets
gtk_source_buffer_get_highlight_matching_brackets
gtk_source_buffer_set_style_scheme
gtk_source_buffer_get_style_scheme
-gtk_source_buffer_get_max_undo_levels
-gtk_source_buffer_set_max_undo_levels
-gtk_source_buffer_redo
-gtk_source_buffer_undo
-gtk_source_buffer_can_redo
-gtk_source_buffer_can_undo
-gtk_source_buffer_begin_not_undoable_action
-gtk_source_buffer_end_not_undoable_action
gtk_source_buffer_ensure_highlight
gtk_source_buffer_create_source_mark
gtk_source_buffer_forward_iter_to_source_mark
@@ -34,8 +26,36 @@ gtk_source_buffer_iter_has_context_class
gtk_source_buffer_get_context_classes_at_iter
gtk_source_buffer_iter_forward_to_context_class_toggle
gtk_source_buffer_iter_backward_to_context_class_toggle
+<SUBSECTION>
+gtk_source_buffer_get_max_undo_levels
+gtk_source_buffer_set_max_undo_levels
+gtk_source_buffer_redo
+gtk_source_buffer_undo
+gtk_source_buffer_can_redo
+gtk_source_buffer_can_undo
+gtk_source_buffer_begin_not_undoable_action
+gtk_source_buffer_end_not_undoable_action
gtk_source_buffer_get_undo_manager
gtk_source_buffer_set_undo_manager
+<SUBSECTION>
+gtk_source_buffer_set_search_text
+gtk_source_buffer_get_search_text
+gtk_source_buffer_set_case_sensitive_search
+gtk_source_buffer_get_case_sensitive_search
+gtk_source_buffer_set_search_at_word_boundaries
+gtk_source_buffer_get_search_at_word_boundaries
+gtk_source_buffer_set_search_wrap_around
+gtk_source_buffer_get_search_wrap_around
+gtk_source_buffer_get_search_occurrences_count
+gtk_source_buffer_get_search_occurrence_position
+gtk_source_buffer_forward_search
+gtk_source_buffer_forward_search_async
+gtk_source_buffer_forward_search_finish
+gtk_source_buffer_backward_search
+gtk_source_buffer_backward_search_async
+gtk_source_buffer_backward_search_finish
+gtk_source_buffer_search_replace
+gtk_source_buffer_search_replace_all
<SUBSECTION Standard>
GtkSourceBufferClass
GTK_SOURCE_IS_BUFFER
@@ -600,3 +620,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..bea13a1 100644
--- a/gtksourceview/gtksourcebuffer.c
+++ b/gtksourceview/gtksourcebuffer.c
@@ -30,10 +30,10 @@
#include <string.h>
#include <gtk/gtk.h>
+#include "gtksourcebuffer.h"
#include "gtksourceview-i18n.h"
#include "gtksourcelanguage.h"
#include "gtksourcelanguage-private.h"
-#include "gtksourcebuffer.h"
#include "gtksourceundomanager.h"
#include "gtksourceview-marshal.h"
#include "gtksourcestylescheme.h"
@@ -42,6 +42,7 @@
#include "gtksourceundomanagerdefault.h"
#include "gtksourceview-typebuiltins.h"
#include "gtksourcemark.h"
+#include "gtksourcesearch.h"
/**
* SECTION:buffer
@@ -49,32 +50,71 @@
* @Title: GtkSourceBuffer
* @See_also: #GtkTextBuffer, #GtkSourceView
*
- * The #GtkSourceBuffer object is the model for #GtkSourceView widgets.
- * It extends the #GtkTextBuffer object by adding features useful to display
- * and edit source code as syntax highlighting and bracket matching. It
- * also implements support for undo/redo operations.
- *
- * To create a #GtkSourceBuffer use gtk_source_buffer_new() or
- * gtk_source_buffer_new_with_language(). The second form is just a convenience
- * function which allows you to initially set a #GtkSourceLanguage.
- *
- * By default highlighting is enabled, but you can disable it with
- * gtk_source_buffer_set_highlight_syntax().
- *
- * A custom #GtkSourceUndoManager can be implemented and set with
- * gtk_source_buffer_set_undo_manager(). However the default implementation
- * should be suitable for most uses. By default, actions that can be undone or
- * redone are defined as groups of operations between a call to
- * gtk_text_buffer_begin_user_action() and gtk_text_buffer_end_user_action(). In
- * general, this happens whenever the user presses any key which modifies the
- * buffer. But the default undo manager will try to merge similar consecutive
- * actions, such as multiple character insertions on the same line, into one
- * action. But, inserting a newline starts a new action.
- *
- * The default undo manager remembers the "modified" state of the buffer, and
- * restore it when an action is undone or redone. It can be useful in a text
- * editor to know whether the file is saved. See gtk_text_buffer_get_modified()
- * and gtk_text_buffer_set_modified().
+ * <para>
+ * The #GtkSourceBuffer object is the model for #GtkSourceView widgets.
+ * It extends the #GtkTextBuffer object by adding features useful to display
+ * and edit source code such as syntax highlighting and bracket matching. It
+ * also implements support for undo/redo operations, and for the search and
+ * replace.
+ * </para>
+ * <para>
+ * To create a #GtkSourceBuffer use gtk_source_buffer_new() or
+ * gtk_source_buffer_new_with_language(). The second form is just a convenience
+ * function which allows you to initially set a #GtkSourceLanguage.
+ * </para>
+ * <para>
+ * By default highlighting is enabled, but you can disable it with
+ * gtk_source_buffer_set_highlight_syntax().
+ * </para>
+ * <refsect2>
+ * <title>Undo and Redo</title>
+ * <para>
+ * A custom #GtkSourceUndoManager can be implemented and set with
+ * gtk_source_buffer_set_undo_manager(). However the default implementation
+ * should be suitable for most uses. By default, actions that can be undone or
+ * redone are defined as groups of operations between a call to
+ * gtk_text_buffer_begin_user_action() and gtk_text_buffer_end_user_action(). In
+ * general, this happens whenever the user presses any key which modifies the
+ * buffer. But the default undo manager will try to merge similar consecutive
+ * actions, such as multiple character insertions on the same line, into one
+ * action. But, inserting a newline starts a new action.
+ * </para>
+ * <para>
+ * The default undo manager remembers the "modified" state of the buffer, and
+ * restore it when an action is undone or redone. It can be useful in a text
+ * editor to know whether the file is saved. See gtk_text_buffer_get_modified()
+ * and gtk_text_buffer_set_modified().
+ * </para>
+ * </refsect2>
+ * <refsect2>
+ * <title>Search and Replace</title>
+ * <para>
+ * To set the text to search, use gtk_source_buffer_set_search_text(). The
+ * search occurrences will be highlighted, and the total number of
+ * occurrences can be retrieved with
+ * gtk_source_buffer_get_search_occurrences_count(). The buffer is scanned
+ * asynchronously, so it doesn't block the user interface. For each search,
+ * the buffer is scanned at most once. After that, navigating through the
+ * occurrences doesn't require to re-scan the buffer entirely.
+ * </para>
+ * <para>
+ * You can tune the search with the following properties:
+ * #GtkSourceBuffer:case-sensitive-search,
+ * #GtkSourceBuffer:search-at-word-boundaries and
+ * #GtkSourceBuffer:search-wrap-around.
+ * </para>
+ * <para>
+ * To search forward, use gtk_source_buffer_forward_search() or
+ * gtk_source_buffer_forward_search_async() for the asynchronous version.
+ * The backward search is done similarly. To replace a search match, or all
+ * matches, use gtk_source_buffer_search_replace() and
+ * gtk_source_buffer_search_replace_all().
+ * </para>
+ * <para>
+ * To know the position of a certain match, use
+ * gtk_source_buffer_get_search_occurrence_position().
+ * </para>
+ * </refsect2>
*/
/*
@@ -120,7 +160,12 @@ enum {
PROP_MAX_UNDO_LEVELS,
PROP_LANGUAGE,
PROP_STYLE_SCHEME,
- PROP_UNDO_MANAGER
+ PROP_UNDO_MANAGER,
+ PROP_SEARCH_TEXT,
+ PROP_SEARCH_OCCURRENCES_COUNT,
+ PROP_CASE_SENSITIVE_SEARCH,
+ PROP_SEARCH_AT_WORD_BOUNDARIES,
+ PROP_SEARCH_WRAP_AROUND
};
struct _GtkSourceBufferPrivate
@@ -140,6 +185,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 +382,87 @@ 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-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));
+
+ /**
+ * GtkSourceBuffer:case-sensitive-search:
+ *
+ * Whether the search is case sensitive.
+ *
+ * Since: 3.10
+ */
+ g_object_class_install_property (object_class,
+ PROP_CASE_SENSITIVE_SEARCH,
+ g_param_spec_boolean ("case-sensitive-search",
+ _("Case sensitive search"),
+ _("Case sensitive search"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+ /**
+ * GtkSourceBuffer:search-at-word-boundaries:
+ *
+ * If %TRUE, a search match must start and end a word. The match can
+ * span multiple words.
+ *
+ * Since: 3.10
+ */
+ g_object_class_install_property (object_class,
+ PROP_SEARCH_AT_WORD_BOUNDARIES,
+ g_param_spec_boolean ("search-at-word-boundaries",
+ _("Search at word boundaries"),
+ _("Search at word boundaries"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+ /**
+ * GtkSourceBuffer:search-wrap-around:
+ *
+ * For a forward search, continue at the beginning of the buffer if no
+ * search occurrence is found. For a backward search, continue at the
+ * end of the buffer.
+ *
+ * Since: 3.10
+ */
+ g_object_class_install_property (object_class,
+ PROP_SEARCH_WRAP_AROUND,
+ g_param_spec_boolean ("search-wrap-around",
+ _("Search: wrap around"),
+ _("Search: wrap around"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
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 +604,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 +648,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 +697,26 @@ 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_CASE_SENSITIVE_SEARCH:
+ _gtk_source_search_set_case_sensitive (source_buffer->priv->search,
+ g_value_get_boolean (value));
+ break;
+
+ case PROP_SEARCH_AT_WORD_BOUNDARIES:
+ _gtk_source_search_set_at_word_boundaries (source_buffer->priv->search,
+ g_value_get_boolean (value));
+ break;
+
+ case PROP_SEARCH_WRAP_AROUND:
+ _gtk_source_search_set_wrap_around (source_buffer->priv->search,
+ g_value_get_boolean (value));
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -621,6 +772,26 @@ 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_OCCURRENCES_COUNT:
+ g_value_set_uint (value, _gtk_source_search_get_occurrences_count
(source_buffer->priv->search));
+ break;
+
+ case PROP_CASE_SENSITIVE_SEARCH:
+ g_value_set_boolean (value, _gtk_source_search_get_case_sensitive
(source_buffer->priv->search));
+ break;
+
+ case PROP_SEARCH_AT_WORD_BOUNDARIES:
+ g_value_set_boolean (value, _gtk_source_search_get_at_word_boundaries
(source_buffer->priv->search));
+ break;
+
+ case PROP_SEARCH_WRAP_AROUND:
+ g_value_set_boolean (value, _gtk_source_search_get_wrap_around
(source_buffer->priv->search));
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -1532,10 +1703,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 +2654,486 @@ gtk_source_buffer_get_undo_manager (GtkSourceBuffer *buffer)
return buffer->priv->undo_manager;
}
+
+/**
+ * gtk_source_buffer_set_search_text:
+ * @buffer: a #GtkSourceBuffer.
+ * @text: the nul-terminated 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_case_sensitive_search:
+ * @buffer: a #GtkSourceBuffer.
+ * @case_sensitive: the setting.
+ *
+ * Enables or disables the case sensitivity for the search.
+ *
+ * Since: 3.10
+ */
+void
+gtk_source_buffer_set_case_sensitive_search (GtkSourceBuffer *buffer,
+ gboolean case_sensitive)
+{
+ gboolean cur_val;
+
+ g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
+
+ case_sensitive = case_sensitive != FALSE;
+
+ cur_val = _gtk_source_search_get_case_sensitive (buffer->priv->search);
+
+ if (cur_val != case_sensitive)
+ {
+ _gtk_source_search_set_case_sensitive (buffer->priv->search,
+ case_sensitive);
+
+ g_object_notify (G_OBJECT (buffer), "case-sensitive-search");
+ }
+}
+
+/**
+ * gtk_source_buffer_get_case_sensitive_search:
+ * @buffer: a #GtkSourceBuffer.
+ *
+ * Returns: whether the search is case sensitive.
+ * Since: 3.10
+ */
+gboolean
+gtk_source_buffer_get_case_sensitive_search (GtkSourceBuffer *buffer)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), FALSE);
+
+ return _gtk_source_search_get_case_sensitive (buffer->priv->search);
+}
+
+/**
+ * gtk_source_buffer_set_search_at_word_boundaries:
+ * @buffer: a #GtkSourceBuffer.
+ * @at_word_boundaries: the setting.
+ *
+ * Change whether the search is done at word boundaries. If @at_word_boundaries
+ * is %TRUE, a search match must start and end a word. The match can span
+ * multiple words. See also gtk_text_iter_starts_word() and
+ * gtk_text_iter_ends_word().
+ *
+ * Since: 3.10
+ */
+void
+gtk_source_buffer_set_search_at_word_boundaries (GtkSourceBuffer *buffer,
+ gboolean at_word_boundaries)
+{
+ gboolean cur_val;
+
+ g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
+
+ at_word_boundaries = at_word_boundaries != FALSE;
+
+ cur_val = _gtk_source_search_get_at_word_boundaries (buffer->priv->search);
+
+ if (cur_val != at_word_boundaries)
+ {
+ _gtk_source_search_set_at_word_boundaries (buffer->priv->search,
+ at_word_boundaries);
+
+ g_object_notify (G_OBJECT (buffer), "search-at-word-boundaries");
+ }
+}
+
+/**
+ * gtk_source_buffer_get_search_at_word_boundaries:
+ * @buffer: a #GtkSourceBuffer.
+ *
+ * Returns: whether to search at word boundaries.
+ * Since: 3.10
+ */
+gboolean
+gtk_source_buffer_get_search_at_word_boundaries (GtkSourceBuffer *buffer)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), FALSE);
+
+ return _gtk_source_search_get_at_word_boundaries (buffer->priv->search);
+}
+
+/**
+ * gtk_source_buffer_set_search_wrap_around:
+ * @buffer: a #GtkSourceBuffer.
+ * @wrap_around: the setting.
+ *
+ * Enables or disables the wrap around search. If @wrap_around is %TRUE, the
+ * forward search continues at the beginning of the buffer if no search
+ * occurrences are found. Similarly, the backward search continues to search at
+ * the end of the buffer.
+ *
+ * Since: 3.10
+ */
+void
+gtk_source_buffer_set_search_wrap_around (GtkSourceBuffer *buffer,
+ gboolean wrap_around)
+{
+ gboolean cur_val;
+
+ g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
+
+ wrap_around = wrap_around != FALSE;
+
+ cur_val = _gtk_source_search_get_wrap_around (buffer->priv->search);
+
+ if (cur_val != wrap_around)
+ {
+ _gtk_source_search_set_wrap_around (buffer->priv->search,
+ wrap_around);
+
+ g_object_notify (G_OBJECT (buffer), "search-wrap-around");
+ }
+}
+
+/**
+ * gtk_source_buffer_get_search_wrap_around:
+ * @buffer: a #GtkSourceBuffer.
+ *
+ * Returns: whether to wrap around the search.
+ * Since: 3.10
+ */
+gboolean
+gtk_source_buffer_get_search_wrap_around (GtkSourceBuffer *buffer)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), FALSE);
+
+ return _gtk_source_search_get_wrap_around (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);
+}
+
+/**
+ * gtk_source_buffer_get_search_occurrence_position:
+ * @buffer: a #GtkSourceBuffer.
+ * @match_start: the start of the occurrence.
+ * @match_end: the end of the occurrence.
+ *
+ * Gets the position of a search occurrence. If the buffer is not already fully
+ * scanned, the position may be unknown, and -1 is returned. Therefore you
+ * should call this function when you know that the buffer is fully scanned.
+ *
+ * Returns: the position of the search occurrence. The first occurrence has the
+ * position 1 (not 0). Returns 0 if @match_start and @match_end doesn't delimit
+ * an occurrence. Returns -1 if the position is not yet known.
+ *
+ * Since: 3.10
+ */
+gint
+gtk_source_buffer_get_search_occurrence_position (GtkSourceBuffer *buffer,
+ const GtkTextIter *match_start,
+ const GtkTextIter *match_end)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), -1);
+ g_return_val_if_fail (match_start != NULL, -1);
+ g_return_val_if_fail (match_end != NULL, -1);
+
+ return _gtk_source_search_get_occurrence_position (buffer->priv->search,
+ match_start,
+ match_end);
+}
+
+/**
+ * gtk_source_buffer_forward_search:
+ * @buffer: a #GtkSourceBuffer.
+ * @iter: start of search.
+ * @match_start: return location for start of match, or %NULL.
+ * @match_end: return location for end of match, or %NULL.
+ *
+ * Synchronous forward search.
+ *
+ * Returns: whether a match was found.
+ * Since: 3.10
+ */
+gboolean
+gtk_source_buffer_forward_search (GtkSourceBuffer *buffer,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), FALSE);
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ return _gtk_source_search_forward (buffer->priv->search,
+ iter,
+ match_start,
+ match_end);
+}
+
+/**
+ * gtk_source_buffer_forward_search_async:
+ * @buffer: a #GtkSourceBuffer.
+ * @iter: start of search.
+ * @cancellable: a #GCancellable, or %NULL.
+ * @callback: a #GAsyncReadyCallback to call when the operation is finished.
+ * @user_data: the data to pass to the @callback function.
+ *
+ * Asynchronous forward search. See the #GAsyncResult documentation to know
+ * how to use this function.
+ *
+ * If the operation is cancelled, the @callback will only be called if
+ * @cancellable was not %NULL. gtk_source_buffer_forward_search_async() takes
+ * ownership of @cancellable, so you can unref it after calling this function.
+ *
+ * Since: 3.10
+ */
+void
+gtk_source_buffer_forward_search_async (GtkSourceBuffer *buffer,
+ const GtkTextIter *iter,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
+ g_return_if_fail (iter != NULL);
+
+ _gtk_source_search_forward_async (buffer->priv->search,
+ iter,
+ cancellable,
+ callback,
+ user_data);
+}
+
+/**
+ * gtk_source_buffer_forward_search_finish:
+ * @buffer: a #GtkSourceBuffer.
+ * @result: a #GAsyncResult.
+ * @match_start: return location for start of match, or %NULL.
+ * @match_end: return location for end of match, or %NULL.
+ * @error: a #GError, or %NULL.
+ *
+ * Finishes a forward search started with
+ * gtk_source_buffer_forward_search_async().
+ *
+ * Returns: whether a match was found.
+ * Since: 3.10
+ */
+gboolean
+gtk_source_buffer_forward_search_finish (GtkSourceBuffer *buffer,
+ GAsyncResult *result,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ GError **error)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), FALSE);
+
+ return _gtk_source_search_forward_finish (buffer->priv->search,
+ result,
+ match_start,
+ match_end,
+ error);
+}
+
+/**
+ * gtk_source_buffer_backward_search:
+ * @buffer: a #GtkSourceBuffer.
+ * @iter: start of search.
+ * @match_start: return location for start of match, or %NULL.
+ * @match_end: return location for end of match, or %NULL.
+ *
+ * Synchronous backward search.
+ *
+ * Returns: whether a match was found.
+ * Since: 3.10
+ */
+gboolean
+gtk_source_buffer_backward_search (GtkSourceBuffer *buffer,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), FALSE);
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ return _gtk_source_search_backward (buffer->priv->search,
+ iter,
+ match_start,
+ match_end);
+}
+
+/**
+ * gtk_source_buffer_backward_search_async:
+ * @buffer: a #GtkSourceBuffer.
+ * @iter: start of search.
+ * @cancellable: a #GCancellable, or %NULL.
+ * @callback: a #GAsyncReadyCallback to call when the operation is finished.
+ * @user_data: the data to pass to the @callback function.
+ *
+ * Asynchronous backward search. See the #GAsyncResult documentation to know
+ * how to use this function.
+ *
+ * If the operation is cancelled, the @callback will only be called if
+ * @cancellable was not %NULL. gtk_source_buffer_backward_search_async() takes
+ * ownership of @cancellable, so you can unref it after calling this function.
+ *
+ * Since: 3.10
+ */
+void
+gtk_source_buffer_backward_search_async (GtkSourceBuffer *buffer,
+ const GtkTextIter *iter,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer));
+ g_return_if_fail (iter != NULL);
+
+ _gtk_source_search_backward_async (buffer->priv->search,
+ iter,
+ cancellable,
+ callback,
+ user_data);
+}
+
+/**
+ * gtk_source_buffer_backward_search_finish:
+ * @buffer: a #GtkSourceBuffer.
+ * @result: a #GAsyncResult.
+ * @match_start: return location for start of match, or %NULL.
+ * @match_end: return location for end of match, or %NULL.
+ * @error: a #GError, or %NULL.
+ *
+ * Finishes a backward search started with
+ * gtk_source_buffer_backward_search_async().
+ *
+ * Returns: whether a match was found.
+ * Since: 3.10
+ */
+gboolean
+gtk_source_buffer_backward_search_finish (GtkSourceBuffer *buffer,
+ GAsyncResult *result,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ GError **error)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), FALSE);
+
+ return _gtk_source_search_backward_finish (buffer->priv->search,
+ result,
+ match_start,
+ match_end,
+ error);
+}
+
+/**
+ * gtk_source_buffer_search_replace:
+ * @buffer: a #GtkSourceBuffer.
+ * @match_start: the start of the match to replace.
+ * @match_end: the end of the match to replace.
+ * @replace: the replacement text.
+ * @replace_length: the length of @replace in bytes, or -1.
+ *
+ * Replaces a search match by another text. If @match_start and @match_end
+ * doesn't correspond to a search match, %FALSE is returned.
+ *
+ * Returns: whether the match has been replaced.
+ * Since: 3.10
+ */
+gboolean
+gtk_source_buffer_search_replace (GtkSourceBuffer *buffer,
+ const GtkTextIter *match_start,
+ const GtkTextIter *match_end,
+ const gchar *replace,
+ gint replace_length)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), FALSE);
+ g_return_val_if_fail (match_start != NULL, FALSE);
+ g_return_val_if_fail (match_end != NULL, FALSE);
+ g_return_val_if_fail (replace != NULL, FALSE);
+
+ return _gtk_source_search_replace (buffer->priv->search,
+ match_start,
+ match_end,
+ replace,
+ replace_length);
+}
+
+/**
+ * gtk_source_buffer_search_replace_all:
+ * @buffer: a #GtkSourceBuffer.
+ * @replace: the replacement text.
+ * @replace_length: the length of @replace in bytes, or -1.
+ *
+ * Replaces all search matches by another text.
+ *
+ * Returns: the number of replaced matches.
+ * Since: 3.10
+ */
+guint
+gtk_source_buffer_search_replace_all (GtkSourceBuffer *buffer,
+ const gchar *replace,
+ gint replace_length)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_BUFFER (buffer), 0);
+ g_return_val_if_fail (replace != NULL, 0);
+
+ return _gtk_source_search_replace_all (buffer->priv->search,
+ replace,
+ replace_length);
+}
diff --git a/gtksourceview/gtksourcebuffer.h b/gtksourceview/gtksourcebuffer.h
index 80b365e..e9fd66e 100644
--- a/gtksourceview/gtksourcebuffer.h
+++ b/gtksourceview/gtksourcebuffer.h
@@ -175,6 +175,78 @@ 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_case_sensitive_search (GtkSourceBuffer
*buffer,
+ gboolean
case_sensitive);
+
+gboolean gtk_source_buffer_get_case_sensitive_search (GtkSourceBuffer
*buffer);
+
+void gtk_source_buffer_set_search_at_word_boundaries (GtkSourceBuffer
*buffer,
+ gboolean
at_word_boundaries);
+
+gboolean gtk_source_buffer_get_search_at_word_boundaries (GtkSourceBuffer
*buffer);
+
+void gtk_source_buffer_set_search_wrap_around (GtkSourceBuffer
*buffer,
+ gboolean
wrap_around);
+
+gboolean gtk_source_buffer_get_search_wrap_around (GtkSourceBuffer
*buffer);
+
+guint gtk_source_buffer_get_search_occurrences_count (GtkSourceBuffer
*buffer);
+
+gint gtk_source_buffer_get_search_occurrence_position (GtkSourceBuffer
*buffer,
+ const GtkTextIter
*match_start,
+ const GtkTextIter
*match_end);
+
+
+gboolean gtk_source_buffer_forward_search (GtkSourceBuffer *buffer,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end);
+
+void gtk_source_buffer_forward_search_async (GtkSourceBuffer *buffer,
+ const GtkTextIter *iter,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean gtk_source_buffer_forward_search_finish(GtkSourceBuffer *buffer,
+ GAsyncResult *result,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ GError **error);
+
+gboolean gtk_source_buffer_backward_search (GtkSourceBuffer *buffer,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end);
+
+void gtk_source_buffer_backward_search_async(GtkSourceBuffer *buffer,
+ const GtkTextIter *iter,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean gtk_source_buffer_backward_search_finish
+ (GtkSourceBuffer *buffer,
+ GAsyncResult *result,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ GError **error);
+
+gboolean gtk_source_buffer_search_replace (GtkSourceBuffer *buffer,
+ const GtkTextIter *match_start,
+ const GtkTextIter *match_end,
+ const gchar *replace,
+ gint replace_length);
+
+guint gtk_source_buffer_search_replace_all (GtkSourceBuffer *buffer,
+ const gchar *replace,
+ gint replace_length);
+
/* 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..c512083
--- /dev/null
+++ b/gtksourceview/gtksourcesearch.c
@@ -0,0 +1,2125 @@
+/* -*- 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 options), we
+ * have to update the highlighting and the properties values (the number of
+ * occurrences). To do so, a simple solution is to first remove all the
+ * found_tag, so we have a clean buffer to analyze. The problem with this
+ * solution is that there is some flickering when the user modifies the text to
+ * search, because removing all the found_tag's can take some time. So we keep
+ * the old found_tag's, and when we must highlight the matches in a certain
+ * region, we first remove the found_tag's in this region and then we highlight
+ * the newly found matches by applying the found_tag to them.
+ *
+ * If we only want to highlight the matches, without counting the number of
+ * occurrences, a good solution would be to highlight only the visible region of
+ * the buffer on the screen. So it would be useless to always scan all the
+ * buffer.
+ *
+ * But we actually want the number of occurrences! So we have to scan all the
+ * buffer. When the state of the search changes, an idle callback is installed,
+ * which will scan the buffer to highlight the matches. To avoid flickering, the
+ * visible region on the screen is put in a higher priority region to highlight,
+ * so the idle callback will first scan this region.
+ *
+ * Why highlighting the non-visible matches? What we want is to (1) highlight
+ * the visible matches and (2) count the number of occurrences. The code would
+ * indeed be simpler if these two tasks were clearly separated (in two different
+ * idle callbacks, with different regions to scan). With this simpler solution,
+ * we would always use forward_search() and backward_search() to navigate
+ * through the occurrences. But we can do better than that!
+ * forward_to_tag_toggle() and backward_to_tag_toggle() are far more efficient:
+ * once the buffer has been scanned, going to the previous or the next
+ * occurrence is done in O(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.
+ * A lower value means more overhead when scanning the buffer asynchronously.
+ */
+#define SCAN_BATCH_SIZE 100
+
+struct _GtkSourceSearchPrivate
+{
+ GtkTextBuffer *buffer;
+
+ /* 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;
+
+ /* An asynchronous running task. task_region has a higher priority than
+ * scan_region, but a lower priority than high_priority_region.
+ */
+ GTask *task;
+ GtkTextRegion *task_region;
+
+ gulong idle_scan_id;
+
+ guint occurrences_count;
+
+ GtkTextTag *found_tag;
+
+ /* State of the search. If text is NULL, the search is disabled. */
+ gchar *text;
+ gint text_nb_lines;
+ GtkTextSearchFlags flags;
+ guint at_word_boundaries : 1;
+ guint wrap_around : 1;
+};
+
+/* Data for the asynchronous forward and backward search tasks. */
+typedef struct
+{
+ GtkTextMark *start_at;
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+ guint found : 1;
+ guint wrapped_around : 1;
+
+ /* forward or backward */
+ guint is_forward : 1;
+} ForwardBackwardData;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceSearch, _gtk_source_search, G_TYPE_OBJECT);
+
+static void install_idle_scan (GtkSourceSearch *search);
+
+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 the tag
+ * table is created too early.
+ */
+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;
+}
+
+/* Sets @start and @end to the last non-empty subregion.
+ * Returns FALSE if the region is empty.
+ */
+static gboolean
+get_last_subregion (GtkTextRegion *region,
+ GtkTextIter *start,
+ GtkTextIter *end)
+{
+ GtkTextRegionIterator region_iter;
+ gboolean found = FALSE;
+
+ if (region == NULL)
+ {
+ return FALSE;
+ }
+
+ gtk_text_region_get_iterator (region, ®ion_iter, 0);
+
+ while (!gtk_text_region_iterator_is_end (®ion_iter))
+ {
+ GtkTextIter start_subregion;
+ GtkTextIter end_subregion;
+
+ gtk_text_region_iterator_get_subregion (®ion_iter,
+ &start_subregion,
+ &end_subregion);
+
+ if (!gtk_text_iter_equal (&start_subregion, &end_subregion))
+ {
+ found = TRUE;
+ *start = start_subregion;
+ *end = end_subregion;
+ }
+
+ gtk_text_region_iterator_next (®ion_iter);
+ }
+
+ return found;
+}
+
+static void
+clear_task (GtkSourceSearch *search)
+{
+ if (search->priv->task_region != NULL)
+ {
+ gtk_text_region_destroy (search->priv->task_region, TRUE);
+ search->priv->task_region = NULL;
+ }
+
+ if (search->priv->task != NULL)
+ {
+ GCancellable *cancellable = g_task_get_cancellable (search->priv->task);
+
+ if (cancellable != NULL)
+ {
+ g_cancellable_cancel (cancellable);
+ g_task_return_error_if_cancelled (search->priv->task);
+ }
+
+ g_clear_object (&search->priv->task);
+ }
+}
+
+static void
+clear_search (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;
+ }
+
+ clear_task (search);
+
+ search->priv->occurrences_count = 0;
+}
+
+static gboolean
+basic_forward_search (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ const GtkTextIter *limit)
+{
+ GtkTextIter begin_search = *iter;
+
+ if (search->priv->text == NULL)
+ {
+ return FALSE;
+ }
+
+ while (TRUE)
+ {
+ gboolean found = gtk_text_iter_forward_search (&begin_search,
+ search->priv->text,
+ search->priv->flags,
+ match_start,
+ match_end,
+ limit);
+
+ if (!found || !search->priv->at_word_boundaries)
+ {
+ return found;
+ }
+
+ if (gtk_text_iter_starts_word (match_start) &&
+ gtk_text_iter_ends_word (match_end))
+ {
+ return TRUE;
+ }
+
+ begin_search = *match_end;
+ }
+}
+
+static gboolean
+basic_backward_search (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ const GtkTextIter *limit)
+{
+ GtkTextIter begin_search = *iter;
+
+ if (search->priv->text == NULL)
+ {
+ return FALSE;
+ }
+
+ while (TRUE)
+ {
+ gboolean found = gtk_text_iter_backward_search (&begin_search,
+ search->priv->text,
+ search->priv->flags,
+ match_start,
+ match_end,
+ limit);
+
+ if (!found || !search->priv->at_word_boundaries)
+ {
+ return found;
+ }
+
+ if (gtk_text_iter_starts_word (match_start) &&
+ gtk_text_iter_ends_word (match_end))
+ {
+ return TRUE;
+ }
+
+ begin_search = *match_start;
+ }
+}
+
+static void
+forward_backward_data_free (ForwardBackwardData *data)
+{
+ if (data->start_at != NULL)
+ {
+ GtkTextBuffer *buffer = gtk_text_mark_get_buffer (data->start_at);
+ gtk_text_buffer_delete_mark (buffer, data->start_at);
+ }
+
+ g_slice_free (ForwardBackwardData, data);
+}
+
+/* Returns TRUE if finished. */
+static gboolean
+smart_forward_search_async_step (GtkSourceSearch *search,
+ GtkTextIter *start_at,
+ gboolean *wrapped_around)
+{
+ GtkTextIter iter = *start_at;
+ GtkTextIter limit;
+ GtkTextRegion *region = NULL;
+ ForwardBackwardData *task_data;
+
+ if (gtk_text_iter_is_end (start_at))
+ {
+ if (search->priv->text != NULL &&
+ !*wrapped_around &&
+ search->priv->wrap_around)
+ {
+ gtk_text_buffer_get_start_iter (search->priv->buffer, start_at);
+ *wrapped_around = TRUE;
+ return FALSE;
+ }
+
+ task_data = g_slice_new0 (ForwardBackwardData);
+ task_data->found = FALSE;
+ task_data->is_forward = TRUE;
+ task_data->wrapped_around = *wrapped_around;
+
+ g_task_return_pointer (search->priv->task,
+ task_data,
+ (GDestroyNotify)forward_backward_data_free);
+
+ g_clear_object (&search->priv->task);
+ return TRUE;
+ }
+
+ if (!gtk_text_iter_has_tag (&iter, search->priv->found_tag))
+ {
+ gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag);
+ }
+
+ limit = iter;
+ gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->found_tag);
+
+ if (search->priv->scan_region != NULL)
+ {
+ region = gtk_text_region_intersect (search->priv->scan_region, start_at, &limit);
+ }
+
+ if (is_text_region_empty (region))
+ {
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+ gboolean found;
+
+ found = basic_forward_search (search, &iter, &match_start, &match_end, &limit);
+
+ if (region != NULL)
+ {
+ gtk_text_region_destroy (region, TRUE);
+ }
+
+ if (found)
+ {
+ task_data = g_slice_new0 (ForwardBackwardData);
+ task_data->found = TRUE;
+ task_data->match_start = match_start;
+ task_data->match_end = match_end;
+ task_data->is_forward = TRUE;
+ task_data->wrapped_around = *wrapped_around;
+
+ g_task_return_pointer (search->priv->task,
+ task_data,
+ (GDestroyNotify)forward_backward_data_free);
+
+ g_clear_object (&search->priv->task);
+ return TRUE;
+ }
+
+ *start_at = limit;
+ return FALSE;
+ }
+
+ task_data = g_slice_new0 (ForwardBackwardData);
+ task_data->is_forward = TRUE;
+ task_data->wrapped_around = *wrapped_around;
+ task_data->start_at = gtk_text_buffer_create_mark (search->priv->buffer,
+ NULL,
+ start_at,
+ TRUE);
+
+ g_task_set_task_data (search->priv->task,
+ task_data,
+ (GDestroyNotify)forward_backward_data_free);
+
+ if (search->priv->task_region != NULL)
+ {
+ gtk_text_region_destroy (search->priv->task_region, TRUE);
+ }
+
+ search->priv->task_region = region;
+
+ install_idle_scan (search);
+
+ /* The idle that scan the task region will call
+ * smart_forward_search_async() to continue the task. But for the
+ * moment, we are done.
+ */
+ return TRUE;
+}
+
+static void
+smart_forward_search_async (GtkSourceSearch *search,
+ const GtkTextIter *start_at,
+ gboolean wrapped_around)
+{
+ GtkTextIter iter = *start_at;
+
+ if (search->priv->found_tag == NULL)
+ {
+ init_found_tag (search);
+ }
+
+ /* A recursive function would have been more natural, but a loop is
+ * better to avoid stack overflows.
+ */
+ while (!smart_forward_search_async_step (search, &iter, &wrapped_around));
+}
+
+
+/* Returns TRUE if finished. */
+static gboolean
+smart_backward_search_async_step (GtkSourceSearch *search,
+ GtkTextIter *start_at,
+ gboolean *wrapped_around)
+{
+ GtkTextIter iter = *start_at;
+ GtkTextIter limit;
+ GtkTextRegion *region = NULL;
+ ForwardBackwardData *task_data;
+
+ if (gtk_text_iter_is_start (start_at))
+ {
+ if (search->priv->text != NULL &&
+ !*wrapped_around &&
+ search->priv->wrap_around)
+ {
+ gtk_text_buffer_get_end_iter (search->priv->buffer, start_at);
+ *wrapped_around = TRUE;
+ return FALSE;
+ }
+
+ task_data = g_slice_new0 (ForwardBackwardData);
+ task_data->found = FALSE;
+ task_data->is_forward = FALSE;
+ task_data->wrapped_around = *wrapped_around;
+
+ g_task_return_pointer (search->priv->task,
+ task_data,
+ (GDestroyNotify)forward_backward_data_free);
+
+ g_clear_object (&search->priv->task);
+ return TRUE;
+ }
+
+ if (gtk_text_iter_begins_tag (&iter, search->priv->found_tag) ||
+ (!gtk_text_iter_has_tag (&iter, search->priv->found_tag) &&
+ !gtk_text_iter_ends_tag (&iter, search->priv->found_tag)))
+ {
+ gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag);
+ }
+
+ limit = iter;
+ gtk_text_iter_backward_to_tag_toggle (&limit, search->priv->found_tag);
+
+ if (search->priv->scan_region != NULL)
+ {
+ region = gtk_text_region_intersect (search->priv->scan_region, &limit, start_at);
+ }
+
+ if (is_text_region_empty (region))
+ {
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+ gboolean found;
+
+ found = basic_backward_search (search, &iter, &match_start, &match_end, &limit);
+
+ if (region != NULL)
+ {
+ gtk_text_region_destroy (region, TRUE);
+ }
+
+ if (found)
+ {
+ task_data = g_slice_new0 (ForwardBackwardData);
+ task_data->found = TRUE;
+ task_data->match_start = match_start;
+ task_data->match_end = match_end;
+ task_data->is_forward = FALSE;
+ task_data->wrapped_around = *wrapped_around;
+
+ g_task_return_pointer (search->priv->task,
+ task_data,
+ (GDestroyNotify)forward_backward_data_free);
+
+ g_clear_object (&search->priv->task);
+ return TRUE;
+ }
+
+ *start_at = limit;
+ return FALSE;
+ }
+
+ task_data = g_slice_new0 (ForwardBackwardData);
+ task_data->is_forward = FALSE;
+ task_data->wrapped_around = *wrapped_around;
+ task_data->start_at = gtk_text_buffer_create_mark (search->priv->buffer,
+ NULL,
+ start_at,
+ TRUE);
+
+ g_task_set_task_data (search->priv->task,
+ task_data,
+ (GDestroyNotify)forward_backward_data_free);
+
+ if (search->priv->task_region != NULL)
+ {
+ gtk_text_region_destroy (search->priv->task_region, TRUE);
+ }
+
+ search->priv->task_region = region;
+
+ install_idle_scan (search);
+
+ /* The idle that scan the task region will call
+ * smart_backward_search_async() to continue the task. But for the
+ * moment, we are done.
+ */
+ return TRUE;
+}
+
+static void
+smart_backward_search_async (GtkSourceSearch *search,
+ const GtkTextIter *start_at,
+ gboolean wrapped_around)
+{
+ GtkTextIter iter = *start_at;
+
+ if (search->priv->found_tag == NULL)
+ {
+ init_found_tag (search);
+ }
+
+ /* A recursive function would have been more natural, but a loop is
+ * better to avoid stack overflows.
+ */
+ while (!smart_backward_search_async_step (search, &iter, &wrapped_around));
+}
+
+/* Adjust the subregion so we are sure that all matches that are visible or
+ * partially visible between @start and @end are highlighted.
+ */
+static void
+adjust_subregion (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. */
+ 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);
+ }
+
+ 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);
+ }
+
+ 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));
+ });
+}
+
+/* Do not take into account the scan_region. Search just with
+ * forward_to_tag_toggle(), and checks that it is not an old match.
+ */
+static gboolean
+smart_forward_search_without_scanning (GtkSourceSearch *search,
+ const GtkTextIter *start_at,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ const GtkTextIter *stop_at)
+{
+ GtkTextIter iter = *start_at;
+
+ g_assert (start_at != NULL);
+ g_assert (stop_at != NULL);
+
+ if (search->priv->text == NULL)
+ {
+ return FALSE;
+ }
+
+ if (search->priv->found_tag == NULL)
+ {
+ init_found_tag (search);
+ }
+
+ while (gtk_text_iter_compare (&iter, stop_at) < 0)
+ {
+ GtkTextIter limit;
+
+ if (!gtk_text_iter_has_tag (&iter, search->priv->found_tag))
+ {
+ gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag);
+ }
+
+ limit = iter;
+ gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->found_tag);
+
+ if (gtk_text_iter_compare (stop_at, &limit) < 0)
+ {
+ limit = *stop_at;
+ }
+
+ if (basic_forward_search (search, &iter, match_start, match_end, &limit))
+ {
+ return TRUE;
+ }
+
+ iter = limit;
+ }
+
+ return FALSE;
+}
+
+/* Remove the occurrences in the range. @start and @end may be adjusted, if they
+ * are in a found_tag region.
+ */
+static void
+remove_occurrences_in_range (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_begins_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_begins_tag (end, search->priv->found_tag))
+ {
+ gtk_text_iter_forward_to_tag_toggle (end, search->priv->found_tag);
+ }
+
+ iter = *start;
+
+ while (smart_forward_search_without_scanning (search, &iter, &match_start, &match_end, end))
+ {
+ if (search->priv->scan_region == NULL)
+ {
+ /* The occurrence has already been scanned, and thus
+ * occurrence_count take it into account. */
+ search->priv->occurrences_count--;
+ }
+ else
+ {
+ 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;
+ }
+
+ gtk_text_buffer_remove_tag (search->priv->buffer,
+ search->priv->found_tag,
+ start,
+ 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->task_region != NULL)
+ {
+ gtk_text_region_subtract (search->priv->task_region, start, end);
+ }
+
+ 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 = basic_forward_search (search, &iter, &match_start, &match_end, limit);
+
+ if (found)
+ {
+ gtk_text_buffer_apply_tag (search->priv->buffer,
+ search->priv->found_tag,
+ &match_start,
+ &match_end);
+
+ search->priv->occurrences_count++;
+ }
+
+ iter = match_end;
+
+ } while (found);
+}
+
+static void
+scan_all_region (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. Begin the scan at the beginning of the region.
+ */
+static void
+scan_region_forward (GtkSourceSearch *search,
+ GtkTextRegion *region)
+{
+ gint nb_remaining_lines = SCAN_BATCH_SIZE;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ while (nb_remaining_lines > 0 &&
+ get_first_subregion (region, &start, &end))
+ {
+ GtkTextIter limit = start;
+ gint start_line;
+ gint limit_line;
+
+ gtk_text_iter_forward_lines (&limit, nb_remaining_lines);
+
+ if (gtk_text_iter_compare (&end, &limit) < 0)
+ {
+ limit = end;
+ }
+
+ scan_subregion (search, &start, &limit);
+
+ start_line = gtk_text_iter_get_line (&start);
+ limit_line = gtk_text_iter_get_line (&limit);
+
+ nb_remaining_lines -= limit_line - start_line;
+ }
+}
+
+/* Same as scan_region_forward(), but begins the scan at the end of the region. */
+static void
+scan_region_backward (GtkSourceSearch *search,
+ GtkTextRegion *region)
+{
+ gint nb_remaining_lines = SCAN_BATCH_SIZE;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ while (nb_remaining_lines > 0 &&
+ get_last_subregion (region, &start, &end))
+ {
+ GtkTextIter limit = end;
+ gint limit_line;
+ gint end_line;
+
+ gtk_text_iter_backward_lines (&limit, nb_remaining_lines);
+
+ if (gtk_text_iter_compare (&limit, &start) < 0)
+ {
+ limit = start;
+ }
+
+ scan_subregion (search, &limit, &end);
+
+ limit_line = gtk_text_iter_get_line (&limit);
+ end_line = gtk_text_iter_get_line (&end);
+
+ nb_remaining_lines -= end_line - limit_line;
+ }
+}
+
+static void
+scan_task_region (GtkSourceSearch *search)
+{
+ ForwardBackwardData *task_data = g_task_get_task_data (search->priv->task);
+ GtkTextIter start_at;
+
+ if (task_data->is_forward)
+ {
+ scan_region_forward (search, search->priv->task_region);
+ }
+ else
+ {
+ scan_region_backward (search, search->priv->task_region);
+ }
+
+ if (search->priv->task_region != NULL)
+ {
+ gtk_text_region_destroy (search->priv->task_region, TRUE);
+ search->priv->task_region = NULL;
+ }
+
+ gtk_text_buffer_get_iter_at_mark (search->priv->buffer,
+ &start_at,
+ task_data->start_at);
+
+ if (task_data->is_forward)
+ {
+ smart_forward_search_async (search,
+ &start_at,
+ task_data->wrapped_around);
+ }
+ else
+ {
+ smart_backward_search_async (search,
+ &start_at,
+ task_data->wrapped_around);
+ }
+}
+
+static gboolean
+idle_scan_cb (GtkSourceSearch *search)
+{
+ if (search->priv->high_priority_region != NULL)
+ {
+ /* Normally the high priority region is not really big, since it
+ * is the visible area on the screen. So we can highlight it in
+ * one batch.
+ */
+ scan_all_region (search, search->priv->high_priority_region);
+
+ gtk_text_region_destroy (search->priv->high_priority_region, TRUE);
+ search->priv->high_priority_region = NULL;
+
+ return G_SOURCE_CONTINUE;
+ }
+
+ if (search->priv->task_region != NULL)
+ {
+ scan_task_region (search);
+ return G_SOURCE_CONTINUE;
+ }
+
+ scan_region_forward (search, search->priv->scan_region);
+
+ if (is_text_region_empty (search->priv->scan_region))
+ {
+ 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 G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+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);
+ }
+}
+
+/* Returns TRUE when finished. */
+static gboolean
+smart_forward_search_step (GtkSourceSearch *search,
+ GtkTextIter *start_at,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ GtkTextIter iter = *start_at;
+ GtkTextIter limit;
+ GtkTextRegion *region = NULL;
+
+ if (!gtk_text_iter_has_tag (&iter, search->priv->found_tag))
+ {
+ gtk_text_iter_forward_to_tag_toggle (&iter, search->priv->found_tag);
+ }
+
+ limit = iter;
+ gtk_text_iter_forward_to_tag_toggle (&limit, search->priv->found_tag);
+
+ if (search->priv->scan_region != NULL)
+ {
+ region = gtk_text_region_intersect (search->priv->scan_region, start_at, &limit);
+ }
+
+ if (is_text_region_empty (region))
+ {
+ gboolean found = basic_forward_search (search, &iter, match_start, match_end, &limit);
+
+ if (region != NULL)
+ {
+ gtk_text_region_destroy (region, TRUE);
+ }
+
+ if (found)
+ {
+ return TRUE;
+ }
+
+ *start_at = limit;
+ return FALSE;
+ }
+
+ scan_all_region (search, region);
+ gtk_text_region_destroy (region, TRUE);
+ return FALSE;
+}
+
+/* Doesn't wrap around. */
+static gboolean
+smart_forward_search (GtkSourceSearch *search,
+ const GtkTextIter *start_at,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ GtkTextIter iter = *start_at;
+
+ if (search->priv->text == NULL)
+ {
+ return FALSE;
+ }
+
+ if (search->priv->found_tag == NULL)
+ {
+ init_found_tag (search);
+ }
+
+ while (!gtk_text_iter_is_end (&iter))
+ {
+ if (smart_forward_search_step (search, &iter, match_start, match_end))
+ {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/* Returns TRUE when finished. */
+static gboolean
+smart_backward_search_step (GtkSourceSearch *search,
+ GtkTextIter *start_at,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ GtkTextIter iter = *start_at;
+ GtkTextIter limit;
+ GtkTextRegion *region = NULL;
+
+ if (gtk_text_iter_begins_tag (&iter, search->priv->found_tag) ||
+ (!gtk_text_iter_has_tag (&iter, search->priv->found_tag) &&
+ !gtk_text_iter_ends_tag (&iter, search->priv->found_tag)))
+ {
+ gtk_text_iter_backward_to_tag_toggle (&iter, search->priv->found_tag);
+ }
+
+ limit = iter;
+ gtk_text_iter_backward_to_tag_toggle (&limit, search->priv->found_tag);
+
+ if (search->priv->scan_region != NULL)
+ {
+ region = gtk_text_region_intersect (search->priv->scan_region, &limit, start_at);
+ }
+
+ if (is_text_region_empty (region))
+ {
+ gboolean found = basic_backward_search (search, &iter, match_start, match_end, &limit);
+
+ if (region != NULL)
+ {
+ gtk_text_region_destroy (region, TRUE);
+ }
+
+ if (found)
+ {
+ return TRUE;
+ }
+
+ *start_at = limit;
+ return FALSE;
+ }
+
+ scan_all_region (search, region);
+ gtk_text_region_destroy (region, TRUE);
+ return FALSE;
+}
+
+/* Doesn't wrap around. */
+static gboolean
+smart_backward_search (GtkSourceSearch *search,
+ const GtkTextIter *start_at,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ GtkTextIter iter = *start_at;
+
+ if (search->priv->text == NULL)
+ {
+ return FALSE;
+ }
+
+ if (search->priv->found_tag == NULL)
+ {
+ init_found_tag (search);
+ }
+
+ while (!gtk_text_iter_is_start (&iter))
+ {
+ if (smart_backward_search_step (search, &iter, match_start, match_end))
+ {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+add_subregion_to_scan (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)
+{
+ clear_task (search);
+
+ 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;
+
+ clear_task (search);
+
+ 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_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);
+
+ search->priv->buffer = NULL;
+
+ 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;
+}
+
+static void
+_gtk_source_search_init (GtkSourceSearch *search)
+{
+ search->priv = _gtk_source_search_get_instance_private (search);
+}
+
+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_number_of_lines (const gchar *text)
+{
+ const gchar *p;
+ gint len;
+ gint nb_of_lines = 1;
+
+ if (text == NULL)
+ {
+ return 0;
+ }
+
+ len = strlen (text);
+ p = text;
+
+ while (len > 0)
+ {
+ gint delimiter;
+ gint next_paragraph;
+
+ pango_find_paragraph_boundary (p, len, &delimiter, &next_paragraph);
+
+ if (delimiter == next_paragraph)
+ {
+ /* not found */
+ break;
+ }
+
+ p += next_paragraph;
+ len -= next_paragraph;
+ nb_of_lines++;
+ }
+
+ return nb_of_lines;
+}
+
+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_number_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_case_sensitive (GtkSourceSearch *search,
+ gboolean case_sensitive)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SEARCH (search));
+
+ if (case_sensitive)
+ {
+ search->priv->flags &= ~GTK_TEXT_SEARCH_CASE_INSENSITIVE;
+ }
+ else
+ {
+ search->priv->flags |= GTK_TEXT_SEARCH_CASE_INSENSITIVE;
+ }
+
+ update (search);
+}
+
+gboolean
+_gtk_source_search_get_case_sensitive (GtkSourceSearch *search)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), FALSE);
+
+ return (search->priv->flags & GTK_TEXT_SEARCH_CASE_INSENSITIVE) == 0;
+}
+
+void
+_gtk_source_search_set_at_word_boundaries (GtkSourceSearch *search,
+ gboolean at_word_boundaries)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SEARCH (search));
+
+ search->priv->at_word_boundaries = at_word_boundaries;
+ update (search);
+}
+
+gboolean
+_gtk_source_search_get_at_word_boundaries (GtkSourceSearch *search)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), FALSE);
+
+ return search->priv->at_word_boundaries;
+}
+
+void
+_gtk_source_search_set_wrap_around (GtkSourceSearch *search,
+ gboolean wrap_around)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SEARCH (search));
+
+ search->priv->wrap_around = wrap_around;
+ update (search);
+}
+
+gboolean
+_gtk_source_search_get_wrap_around (GtkSourceSearch *search)
+{
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), FALSE);
+
+ return search->priv->wrap_around;
+}
+
+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;
+}
+
+gint
+_gtk_source_search_get_occurrence_position (GtkSourceSearch *search,
+ const GtkTextIter *match_start,
+ const GtkTextIter *match_end)
+{
+ GtkTextIter m_start;
+ GtkTextIter m_end;
+ GtkTextIter iter;
+ gboolean found;
+ gint position = 0;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), -1);
+ g_return_val_if_fail (match_start != NULL, -1);
+ g_return_val_if_fail (match_end != NULL, -1);
+
+ if (dispose_has_run (search))
+ {
+ return -1;
+ }
+
+ /* Verify that the occurrence is correct. */
+
+ found = basic_forward_search (search,
+ match_start,
+ &m_start,
+ &m_end,
+ match_end);
+
+ if (!found ||
+ !gtk_text_iter_equal (match_start, &m_start) ||
+ !gtk_text_iter_equal (match_end, &m_end))
+ {
+ return 0;
+ }
+
+ /* Verify that the scan region is empty between the start of the buffer
+ * and the end of the occurrence.
+ */
+
+ gtk_text_buffer_get_start_iter (search->priv->buffer, &iter);
+
+ if (search->priv->scan_region != NULL)
+ {
+ GtkTextRegion *region = gtk_text_region_intersect (search->priv->scan_region,
+ &iter,
+ match_end);
+
+ gboolean empty = is_text_region_empty (region);
+
+ if (region != NULL)
+ {
+ gtk_text_region_destroy (region, TRUE);
+ }
+
+ if (!empty)
+ {
+ return -1;
+ }
+ }
+
+ /* Everything is fine, count the number of previous occurrences. */
+
+ while (smart_forward_search_without_scanning (search, &iter, &m_start, &m_end, match_start))
+ {
+ position++;
+ iter = m_end;
+ }
+
+ return position + 1;
+}
+
+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);
+ }
+}
+
+gboolean
+_gtk_source_search_forward (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ gboolean found;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), FALSE);
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ if (dispose_has_run (search))
+ {
+ return FALSE;
+ }
+
+ found = smart_forward_search (search, iter, match_start, match_end);
+
+ if (!found && search->priv->wrap_around)
+ {
+ GtkTextIter start_iter;
+ gtk_text_buffer_get_start_iter (search->priv->buffer, &start_iter);
+
+ found = smart_forward_search (search, &start_iter, match_start, match_end);
+ }
+
+ return found;
+}
+
+void
+_gtk_source_search_forward_async (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SEARCH (search));
+ g_return_if_fail (iter != NULL);
+
+ if (dispose_has_run (search))
+ {
+ return;
+ }
+
+ clear_task (search);
+ search->priv->task = g_task_new (search->priv->buffer, cancellable, callback, user_data);
+
+ smart_forward_search_async (search, iter, FALSE);
+}
+
+gboolean
+_gtk_source_search_forward_finish (GtkSourceSearch *search,
+ GAsyncResult *result,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ GError **error)
+{
+ ForwardBackwardData *data;
+ gboolean found;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), FALSE);
+
+ if (dispose_has_run (search))
+ {
+ return FALSE;
+ }
+
+ g_return_val_if_fail (g_task_is_valid (result, search->priv->buffer), FALSE);
+
+ data = g_task_propagate_pointer (G_TASK (result), error);
+
+ if (data == NULL)
+ {
+ return FALSE;
+ }
+
+ found = data->found;
+
+ if (found)
+ {
+ if (match_start != NULL)
+ {
+ *match_start = data->match_start;
+ }
+
+ if (match_end != NULL)
+ {
+ *match_end = data->match_end;
+ }
+ }
+
+ forward_backward_data_free (data);
+ return found;
+}
+
+gboolean
+_gtk_source_search_backward (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end)
+{
+ gboolean found;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), FALSE);
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ if (dispose_has_run (search))
+ {
+ return FALSE;
+ }
+
+ found = smart_backward_search (search, iter, match_start, match_end);
+
+ if (!found && search->priv->wrap_around)
+ {
+ GtkTextIter end_iter;
+
+ gtk_text_buffer_get_end_iter (search->priv->buffer, &end_iter);
+
+ found = smart_backward_search (search, &end_iter, match_start, match_end);
+ }
+
+ return found;
+}
+
+void
+_gtk_source_search_backward_async (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTK_SOURCE_IS_SEARCH (search));
+ g_return_if_fail (iter != NULL);
+
+ if (dispose_has_run (search))
+ {
+ return;
+ }
+
+ clear_task (search);
+ search->priv->task = g_task_new (search->priv->buffer, cancellable, callback, user_data);
+
+ smart_backward_search_async (search, iter, FALSE);
+}
+
+gboolean
+_gtk_source_search_backward_finish (GtkSourceSearch *search,
+ GAsyncResult *result,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ GError **error)
+{
+ return _gtk_source_search_forward_finish (search,
+ result,
+ match_start,
+ match_end,
+ error);
+}
+
+gboolean
+_gtk_source_search_replace (GtkSourceSearch *search,
+ const GtkTextIter *match_start,
+ const GtkTextIter *match_end,
+ const gchar *replace,
+ gint replace_length)
+{
+ GtkTextIter start;
+ GtkTextIter end;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), FALSE);
+ g_return_val_if_fail (match_start != NULL, FALSE);
+ g_return_val_if_fail (match_end != NULL, FALSE);
+ g_return_val_if_fail (replace != NULL, FALSE);
+
+ if (dispose_has_run (search))
+ {
+ return FALSE;
+ }
+
+ basic_forward_search (search, match_start, &start, &end, match_end);
+
+ if (!gtk_text_iter_equal (match_start, &start) ||
+ !gtk_text_iter_equal (match_end, &end))
+ {
+ return FALSE;
+ }
+
+ gtk_text_buffer_begin_user_action (search->priv->buffer);
+
+ gtk_text_buffer_delete (search->priv->buffer, &start, &end);
+ gtk_text_buffer_insert (search->priv->buffer, &start, replace, replace_length);
+
+ gtk_text_buffer_end_user_action (search->priv->buffer);
+
+ return TRUE;
+}
+
+guint
+_gtk_source_search_replace_all (GtkSourceSearch *search,
+ const gchar *replace,
+ gint replace_length)
+{
+ GtkTextIter iter;
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+ guint nb_matches_replaced = 0;
+
+ g_return_val_if_fail (GTK_SOURCE_IS_SEARCH (search), 0);
+ g_return_val_if_fail (replace != NULL, 0);
+
+ if (dispose_has_run (search))
+ {
+ return 0;
+ }
+
+ gtk_text_buffer_get_start_iter (search->priv->buffer, &iter);
+
+ gtk_text_buffer_begin_user_action (search->priv->buffer);
+
+ while (smart_forward_search (search, &iter, &match_start, &match_end))
+ {
+ gtk_text_buffer_delete (search->priv->buffer, &match_start, &match_end);
+ gtk_text_buffer_insert (search->priv->buffer, &match_end, replace, replace_length);
+
+ iter = match_end;
+ nb_matches_replaced++;
+ }
+
+ gtk_text_buffer_end_user_action (search->priv->buffer);
+
+ return nb_matches_replaced;
+}
diff --git a/gtksourceview/gtksourcesearch.h b/gtksourceview/gtksourcesearch.h
new file mode 100644
index 0000000..19c6cfa
--- /dev/null
+++ b/gtksourceview/gtksourcesearch.h
@@ -0,0 +1,156 @@
+/* -*- 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_case_sensitive (GtkSourceSearch *search,
+ gboolean
case_sensitive);
+
+G_GNUC_INTERNAL
+gboolean _gtk_source_search_get_case_sensitive (GtkSourceSearch *search);
+
+G_GNUC_INTERNAL
+void _gtk_source_search_set_at_word_boundaries (GtkSourceSearch *search,
+ gboolean
at_word_boundaries);
+
+G_GNUC_INTERNAL
+gboolean _gtk_source_search_get_at_word_boundaries (GtkSourceSearch *search);
+
+G_GNUC_INTERNAL
+void _gtk_source_search_set_wrap_around (GtkSourceSearch *search,
+ gboolean wrap_around);
+
+G_GNUC_INTERNAL
+gboolean _gtk_source_search_get_wrap_around (GtkSourceSearch *search);
+
+G_GNUC_INTERNAL
+guint _gtk_source_search_get_occurrences_count (GtkSourceSearch *search);
+
+G_GNUC_INTERNAL
+gint _gtk_source_search_get_occurrence_position (GtkSourceSearch *search,
+ const GtkTextIter *match_start,
+ const GtkTextIter *match_end);
+
+G_GNUC_INTERNAL
+void _gtk_source_search_update_highlight (GtkSourceSearch *search,
+ const GtkTextIter *start,
+ const GtkTextIter *end,
+ gboolean synchronous);
+
+G_GNUC_INTERNAL
+gboolean _gtk_source_search_forward (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end);
+
+G_GNUC_INTERNAL
+void _gtk_source_search_forward_async (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+G_GNUC_INTERNAL
+gboolean _gtk_source_search_forward_finish (GtkSourceSearch *search,
+ GAsyncResult *result,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ GError **error);
+
+G_GNUC_INTERNAL
+gboolean _gtk_source_search_backward (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end);
+
+G_GNUC_INTERNAL
+void _gtk_source_search_backward_async (GtkSourceSearch *search,
+ const GtkTextIter *iter,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+G_GNUC_INTERNAL
+gboolean _gtk_source_search_backward_finish (GtkSourceSearch *search,
+ GAsyncResult *result,
+ GtkTextIter *match_start,
+ GtkTextIter *match_end,
+ GError **error);
+
+G_GNUC_INTERNAL
+gboolean _gtk_source_search_replace (GtkSourceSearch *search,
+ const GtkTextIter *match_start,
+ const GtkTextIter *match_end,
+ const gchar *replace,
+ gint
replace_length);
+
+G_GNUC_INTERNAL
+guint _gtk_source_search_replace_all (GtkSourceSearch *search,
+ const gchar *replace,
+ gint
replace_length);
+
+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..c589031
--- /dev/null
+++ b/gtksourceview/gtksourceutils.c
@@ -0,0 +1,191 @@
+/* -*- 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().
+ *
+ * Returns: the unescaped @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().
+ *
+ * Returns: the escaped @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..332989b 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,31 @@ 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)
+
+TEST_PROGS += test-search-performances
+test_search_performances_SOURCES = \
+ test-search-performances.c
+test_search_performances_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 +140,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 +158,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-performances.c b/tests/test-search-performances.c
new file mode 100644
index 0000000..42b8a69
--- /dev/null
+++ b/tests/test-search-performances.c
@@ -0,0 +1,183 @@
+/*
+ * test-search-performances.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>
+
+/* This measures the execution times for basic search (with
+ * gtk_text_iter_forward_search()), and "smart" search (the first search with
+ * gtk_text_iter_forward_search(), later searches with
+ * gtk_text_iter_forward_to_tag_toggle()).
+ * For the "smart" search, only the first search is measured. Later searches
+ * are really fast (going to the previous/next occurrence is done in O(1)).
+ * Different search flags are also tested. We can see a big difference between
+ * the case sensitive search and case insensitive.
+ */
+
+static void
+on_notify_search_occurrences_count_cb (GtkSourceBuffer *buffer,
+ GParamSpec *spec,
+ GTimer *timer)
+{
+ g_print ("smart asynchronous search, case sensitive: %lf seconds.\n",
+ g_timer_elapsed (timer, NULL));
+
+ gtk_main_quit ();
+}
+
+int
+main (int argc, char *argv[])
+{
+ GtkSourceBuffer *buffer;
+ GtkTextIter iter;
+ GtkTextIter match_end;
+ GTimer *timer;
+ gint i;
+ GtkTextSearchFlags flags;
+
+ gtk_init (&argc, &argv);
+
+ buffer = gtk_source_buffer_new (NULL);
+
+ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &iter);
+
+ for (i = 0; i < 1000000; i++)
+ {
+ gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer),
+ &iter,
+ "A line of text to fill the text buffer. Is it long enough?\n",
+ -1);
+ }
+
+ gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &iter, "foo\n", -1);
+
+ /* Basic search, no flags */
+
+ timer = g_timer_new ();
+
+ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &iter);
+
+ flags = 0;
+
+ while (gtk_text_iter_forward_search (&iter, "foo", flags, NULL, &match_end, NULL))
+ {
+ iter = match_end;
+ }
+
+ g_timer_stop (timer);
+ g_print ("basic forward search, no flags: %lf seconds.\n",
+ g_timer_elapsed (timer, NULL));
+
+ /* Basic search, with flags always enabled by gsv */
+
+ g_timer_start (timer);
+
+ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &iter);
+
+ flags = GTK_TEXT_SEARCH_VISIBLE_ONLY | GTK_TEXT_SEARCH_TEXT_ONLY;
+
+ while (gtk_text_iter_forward_search (&iter, "foo", flags, NULL, &match_end, NULL))
+ {
+ iter = match_end;
+ }
+
+ g_timer_stop (timer);
+ g_print ("basic forward search, visible and text only flags: %lf seconds.\n",
+ g_timer_elapsed (timer, NULL));
+
+ /* Basic search, with default flags in gsv */
+
+ g_timer_start (timer);
+
+ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &iter);
+
+ flags = GTK_TEXT_SEARCH_VISIBLE_ONLY |
+ GTK_TEXT_SEARCH_TEXT_ONLY |
+ GTK_TEXT_SEARCH_CASE_INSENSITIVE;
+
+ while (gtk_text_iter_forward_search (&iter, "foo", flags, NULL, &match_end, NULL))
+ {
+ iter = match_end;
+ }
+
+ g_timer_stop (timer);
+ g_print ("basic forward search, all flags: %lf seconds.\n",
+ g_timer_elapsed (timer, NULL));
+
+ /* Smart forward search, with default flags in gsv */
+
+ g_timer_start (timer);
+
+ gtk_source_buffer_set_search_wrap_around (buffer, FALSE);
+ gtk_source_buffer_set_search_text (buffer, "foo");
+
+ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &iter);
+
+ while (gtk_source_buffer_forward_search (buffer, &iter, NULL, &match_end))
+ {
+ iter = match_end;
+ }
+
+ g_timer_stop (timer);
+ g_print ("smart synchronous forward search, case insensitive: %lf seconds.\n",
+ g_timer_elapsed (timer, NULL));
+
+ /* Smart forward search, case sensitive */
+
+ g_timer_start (timer);
+
+ gtk_source_buffer_set_search_text (buffer, NULL);
+ gtk_source_buffer_set_case_sensitive_search (buffer, TRUE);
+ gtk_source_buffer_set_search_text (buffer, "foo");
+
+ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &iter);
+
+ while (gtk_source_buffer_forward_search (buffer, &iter, NULL, &match_end))
+ {
+ iter = match_end;
+ }
+
+ g_timer_stop (timer);
+ g_print ("smart synchronous forward search, case sensitive: %lf seconds.\n",
+ g_timer_elapsed (timer, NULL));
+
+ /* Smart search, case sensitive, asynchronous */
+
+ /* The asynchronous overhead doesn't depend on the search flags, it
+ * depends on the maximum number of lines to scan in one batch, and
+ * (obviously), on the buffer size.
+ * You can tune SCAN_BATCH_SIZE in gtksourcesearch.c to see a difference
+ * in the overhead.
+ */
+
+ g_signal_connect (buffer,
+ "notify::search-occurrences-count",
+ G_CALLBACK (on_notify_search_occurrences_count_cb),
+ timer);
+
+ g_timer_start (timer);
+
+ gtk_source_buffer_set_search_text (buffer, NULL);
+ gtk_source_buffer_set_search_text (buffer, "foo");
+
+ gtk_main ();
+ return 0;
+}
diff --git a/tests/test-search-ui.c b/tests/test-search-ui.c
new file mode 100644
index 0000000..b1ac15f
--- /dev/null
+++ b/tests/test-search-ui.c
@@ -0,0 +1,441 @@
+/* -*- 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>
+
+#define TEST_TYPE_SEARCH_UI (test_search_ui_get_type ())
+#define TEST_SEARCH_UI(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TEST_TYPE_SEARCH_UI,
TestSearchUI))
+#define TEST_SEARCH_UI_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TEST_TYPE_SEARCH_UI,
TestSearchUIClass))
+#define TEST_IS_SEARCH_UI(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TEST_TYPE_SEARCH_UI))
+#define TEST_IS_SEARCH_UI_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TEST_TYPE_SEARCH_UI))
+#define TEST_SEARCH_UI_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TEST_TYPE_SEARCH_UI,
TestSearchUIClass))
+
+typedef struct _TestSearchUI TestSearchUI;
+typedef struct _TestSearchUIClass TestSearchUIClass;
+typedef struct _TestSearchUIPrivate TestSearchUIPrivate;
+
+struct _TestSearchUI
+{
+ GtkGrid parent;
+ TestSearchUIPrivate *priv;
+};
+
+struct _TestSearchUIClass
+{
+ GtkGridClass parent_class;
+};
+
+struct _TestSearchUIPrivate
+{
+ GtkSourceView *source_view;
+ GtkSourceBuffer *source_buffer;
+ GtkEntry *replace_entry;
+ GtkLabel *label_occurrences;
+
+ guint idle_update_label_id;
+};
+
+GType test_search_ui_get_type (void);
+
+G_DEFINE_TYPE_WITH_PRIVATE (TestSearchUI, test_search_ui, GTK_TYPE_GRID)
+
+static void
+open_file (TestSearchUI *search,
+ const gchar *filename)
+{
+ gchar *contents;
+ GError *error = NULL;
+ GtkSourceLanguageManager *language_manager;
+ GtkSourceLanguage *language;
+ GtkTextIter iter;
+
+ 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 (search->priv->source_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 (search->priv->source_buffer, language);
+
+ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (search->priv->source_buffer),
+ &iter);
+
+ gtk_text_buffer_select_range (GTK_TEXT_BUFFER (search->priv->source_buffer),
+ &iter,
+ &iter);
+
+ g_free (contents);
+}
+
+static void
+update_label (TestSearchUI *search)
+{
+ guint occurrences_count;
+ GtkTextIter select_start;
+ GtkTextIter select_end;
+ gint occurrence_pos;
+ gchar *text;
+
+ occurrences_count = gtk_source_buffer_get_search_occurrences_count (search->priv->source_buffer);
+
+ gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (search->priv->source_buffer),
+ &select_start,
+ &select_end);
+
+ occurrence_pos = gtk_source_buffer_get_search_occurrence_position (search->priv->source_buffer,
+ &select_start,
+ &select_end);
+
+ if (occurrence_pos > 0)
+ {
+ text = g_strdup_printf ("%d of %u", occurrence_pos, occurrences_count);
+ }
+ else
+ {
+ text = g_strdup_printf ("%u occurrences", occurrences_count);
+ }
+
+ gtk_label_set_text (search->priv->label_occurrences, text);
+ g_free (text);
+}
+
+/* The search entry is a GtkSearchEntry. The "changed" signal is delayed on a
+ * GtkSearchEntry (but not with a simple GtkEntry). That's why the
+ * "notify::text" signal is used instead.
+ * With the "changed" signal, the search highlighting takes some time to be
+ * updated, while with the notify signal, it is immediate.
+ *
+ * See https://bugzilla.gnome.org/show_bug.cgi?id=700229
+ */
+static void
+search_entry_text_notify_cb (TestSearchUI *search,
+ GParamSpec *spec,
+ GtkEntry *entry)
+{
+ const gchar *text = gtk_entry_get_text (entry);
+ gchar *unescaped_text = gtk_source_utils_unescape_search_text (text);
+
+ gtk_source_buffer_set_search_text (search->priv->source_buffer, unescaped_text);
+ g_free (unescaped_text);
+}
+
+static void
+select_search_occurrence (TestSearchUI *search,
+ const GtkTextIter *match_start,
+ const GtkTextIter *match_end)
+{
+ GtkTextMark *insert;
+
+ gtk_text_buffer_select_range (GTK_TEXT_BUFFER (search->priv->source_buffer),
+ match_start,
+ match_end);
+
+ insert = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (search->priv->source_buffer));
+
+ gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (search->priv->source_view),
+ insert);
+}
+
+static void
+backward_search_finished (GtkSourceBuffer *buffer,
+ GAsyncResult *result,
+ TestSearchUI *search)
+{
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+
+ if (gtk_source_buffer_backward_search_finish (buffer,
+ result,
+ &match_start,
+ &match_end,
+ NULL))
+ {
+ select_search_occurrence (search, &match_start, &match_end);
+ }
+}
+
+static void
+button_previous_clicked_cb (TestSearchUI *search,
+ GtkButton *button)
+{
+ GtkTextIter start_at;
+
+ gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (search->priv->source_buffer),
+ &start_at,
+ NULL);
+
+ gtk_source_buffer_backward_search_async (search->priv->source_buffer,
+ &start_at,
+ NULL,
+ (GAsyncReadyCallback)backward_search_finished,
+ search);
+}
+
+static void
+forward_search_finished (GtkSourceBuffer *buffer,
+ GAsyncResult *result,
+ TestSearchUI *search)
+{
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+
+ if (gtk_source_buffer_forward_search_finish (buffer,
+ result,
+ &match_start,
+ &match_end,
+ NULL))
+ {
+ select_search_occurrence (search, &match_start, &match_end);
+ }
+}
+
+static void
+button_next_clicked_cb (TestSearchUI *search,
+ GtkButton *button)
+{
+ GtkTextIter start_at;
+
+ gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (search->priv->source_buffer),
+ NULL,
+ &start_at);
+
+ gtk_source_buffer_forward_search_async (search->priv->source_buffer,
+ &start_at,
+ NULL,
+ (GAsyncReadyCallback)forward_search_finished,
+ search);
+}
+
+static void
+button_replace_clicked_cb (TestSearchUI *search,
+ GtkButton *button)
+{
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+ GtkTextIter iter;
+ GtkEntryBuffer *entry_buffer;
+ gint replace_length;
+
+ gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (search->priv->source_buffer),
+ &match_start,
+ &match_end);
+
+ entry_buffer = gtk_entry_get_buffer (search->priv->replace_entry);
+ replace_length = gtk_entry_buffer_get_bytes (entry_buffer);
+
+ gtk_source_buffer_search_replace (search->priv->source_buffer,
+ &match_start,
+ &match_end,
+ gtk_entry_get_text (search->priv->replace_entry),
+ replace_length);
+
+ gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (search->priv->source_buffer),
+ NULL,
+ &iter);
+
+ gtk_source_buffer_forward_search_async (search->priv->source_buffer,
+ &iter,
+ NULL,
+ (GAsyncReadyCallback)forward_search_finished,
+ search);
+}
+
+static void
+button_replace_all_clicked_cb (TestSearchUI *search,
+ GtkButton *button)
+{
+ GtkEntryBuffer *entry_buffer = gtk_entry_get_buffer (search->priv->replace_entry);
+ gint replace_length = gtk_entry_buffer_get_bytes (entry_buffer);
+
+ gtk_source_buffer_search_replace_all (search->priv->source_buffer,
+ gtk_entry_get_text (search->priv->replace_entry),
+ replace_length);
+}
+
+static gboolean
+update_label_idle_cb (TestSearchUI *search)
+{
+ search->priv->idle_update_label_id = 0;
+
+ update_label (search);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+mark_set_cb (GtkTextBuffer *buffer,
+ GtkTextIter *location,
+ GtkTextMark *mark,
+ TestSearchUI *search)
+{
+ GtkTextMark *insert;
+ GtkTextMark *selection_bound;
+
+ insert = gtk_text_buffer_get_insert (buffer);
+ selection_bound = gtk_text_buffer_get_selection_bound (buffer);
+
+ if ((mark == insert || mark == selection_bound) &&
+ search->priv->idle_update_label_id == 0)
+ {
+ search->priv->idle_update_label_id = g_idle_add ((GSourceFunc)update_label_idle_cb,
+ search);
+ }
+}
+
+static void
+match_case_toggled_cb (TestSearchUI *search,
+ GtkToggleButton *button)
+{
+ gtk_source_buffer_set_case_sensitive_search (search->priv->source_buffer,
+ gtk_toggle_button_get_active (button));
+}
+
+static void
+at_word_boundaries_toggled_cb (TestSearchUI *search,
+ GtkToggleButton *button)
+{
+ gtk_source_buffer_set_search_at_word_boundaries (search->priv->source_buffer,
+ gtk_toggle_button_get_active (button));
+}
+
+static void
+wrap_around_toggled_cb (TestSearchUI *search,
+ GtkToggleButton *button)
+{
+ gtk_source_buffer_set_search_wrap_around (search->priv->source_buffer,
+ gtk_toggle_button_get_active (button));
+}
+
+static void
+test_search_ui_dispose (GObject *object)
+{
+ TestSearchUI *search = TEST_SEARCH_UI (object);
+
+ g_clear_object (&search->priv->source_buffer);
+
+ G_OBJECT_CLASS (test_search_ui_parent_class)->dispose (object);
+}
+
+static void
+test_search_ui_class_init (TestSearchUIClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = test_search_ui_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/gtksourceview/tests/ui/test-search-ui.ui");
+
+ gtk_widget_class_bind_child (widget_class, TestSearchUIPrivate, source_view);
+ gtk_widget_class_bind_child (widget_class, TestSearchUIPrivate, replace_entry);
+ gtk_widget_class_bind_child (widget_class, TestSearchUIPrivate, label_occurrences);
+
+ gtk_widget_class_bind_callback (widget_class, search_entry_text_notify_cb);
+ gtk_widget_class_bind_callback (widget_class, button_previous_clicked_cb);
+ gtk_widget_class_bind_callback (widget_class, button_next_clicked_cb);
+ gtk_widget_class_bind_callback (widget_class, button_replace_clicked_cb);
+ gtk_widget_class_bind_callback (widget_class, button_replace_all_clicked_cb);
+
+ /* It is also possible to bind the properties with
+ * g_object_bind_property(), between the check buttons and the source
+ * buffer. But GtkBuilder and Glade don't support that yet.
+ */
+ gtk_widget_class_bind_callback (widget_class, match_case_toggled_cb);
+ gtk_widget_class_bind_callback (widget_class, at_word_boundaries_toggled_cb);
+ gtk_widget_class_bind_callback (widget_class, wrap_around_toggled_cb);
+}
+
+static void
+test_search_ui_init (TestSearchUI *search)
+{
+ PangoFontDescription *font;
+
+ search->priv = test_search_ui_get_instance_private (search);
+
+ gtk_widget_init_template (GTK_WIDGET (search));
+
+ font = pango_font_description_from_string ("Monospace 10");
+ gtk_widget_override_font (GTK_WIDGET (search->priv->source_view), font);
+ pango_font_description_free (font);
+
+ /* FIXME: bug? Normally the tab width is 8 by default. */
+ gtk_source_view_set_tab_width (search->priv->source_view, 8);
+
+ /* Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=643732:
+ * "Source view is created with a GtkTextBuffer instead of GtkSourceBuffer"
+ */
+ search->priv->source_buffer = gtk_source_buffer_new (NULL);
+
+ gtk_text_view_set_buffer (GTK_TEXT_VIEW (search->priv->source_view),
+ GTK_TEXT_BUFFER (search->priv->source_buffer));
+
+ open_file (search, TOP_SRCDIR "/gtksourceview/gtksourcesearch.c");
+
+ g_signal_connect_swapped (search->priv->source_buffer,
+ "notify::search-occurrences-count",
+ G_CALLBACK (update_label),
+ search);
+
+ g_signal_connect (search->priv->source_buffer,
+ "mark-set",
+ G_CALLBACK (mark_set_cb),
+ search);
+}
+
+static TestSearchUI *
+test_search_ui_new (void)
+{
+ return g_object_new (test_search_ui_get_type (), NULL);
+}
+
+gint
+main (gint argc, gchar *argv[])
+{
+ GtkWidget *window;
+ TestSearchUI *search;
+
+ gtk_init (&argc, &argv);
+
+ window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+
+ gtk_window_set_default_size (GTK_WINDOW (window), 700, 500);
+
+ g_signal_connect (window,
+ "destroy",
+ G_CALLBACK (gtk_main_quit),
+ NULL);
+
+ search = test_search_ui_new ();
+ gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (search));
+
+ gtk_widget_show_all (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..d2d7cc7
--- /dev/null
+++ b/tests/test-search-ui.ui
@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.10 -->
+ <!-- interface-requires gtksourceview 3.0 -->
+ <object class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-go-up</property>
+ <property name="icon_size">1</property>
+ </object>
+ <object class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-go-down</property>
+ <property name="icon_size">1</property>
+ </object>
+ <object class="GtkImage" id="image3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-find-and-replace</property>
+ <property name="icon_size">1</property>
+ </object>
+ <object class="GtkImage" id="image4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-find-and-replace</property>
+ <property name="icon_size">1</property>
+ </object>
+ <template class="TestSearchUI" parent="GtkGrid">
+ <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>
+ <signal name="notify::text" handler="search_entry_text_notify_cb" object="TestSearchUI"
swapped="yes"/>
+ </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">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label">0 occurrences</property>
+ </object>
+ <packing>
+ <property name="left_attach">4</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button_previous">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="image">image1</property>
+ <signal name="clicked" handler="button_previous_clicked_cb" object="TestSearchUI" swapped="yes"/>
+ </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>
+ <child>
+ <object class="GtkButton" id="button_next">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="image">image2</property>
+ <signal name="clicked" handler="button_next_clicked_cb" object="TestSearchUI" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">3</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="draw_indicator">True</property>
+ <signal name="toggled" handler="match_case_toggled_cb" object="TestSearchUI" swapped="yes"/>
+ </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="GtkCheckButton" id="checkbutton_at_word_boundaries">
+ <property name="label">At word boundaries</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="draw_indicator">True</property>
+ <signal name="toggled" handler="at_word_boundaries_toggled_cb" object="TestSearchUI"
swapped="yes"/>
+ </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="GtkCheckButton" id="checkbutton_wrap_around">
+ <property name="label">Wrap around</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>
+ <signal name="toggled" handler="wrap_around_toggled_cb" object="TestSearchUI" swapped="yes"/>
+ </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>
+ <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>
+ <child>
+ <object class="GtkGrid" id="grid4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="column_spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label">Replace:</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="GtkEntry" id="replace_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</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="GtkButton" id="button_replace">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="image">image3</property>
+ <signal name="clicked" handler="button_replace_clicked_cb" object="TestSearchUI" swapped="yes"/>
+ </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>
+ <child>
+ <object class="GtkButton" id="button_replace_all">
+ <property name="label">All</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="image">image4</property>
+ <property name="always_show_image">True</property>
+ <signal name="clicked" handler="button_replace_all_clicked_cb" object="TestSearchUI"
swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">3</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">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </template>
+</interface>
diff --git a/tests/test-search.c b/tests/test-search.c
new file mode 100644
index 0000000..3e725e5
--- /dev/null
+++ b/tests/test-search.c
@@ -0,0 +1,265 @@
+/*
+ * 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);
+}
+
+static void
+test_occurrences_count_multiple_lines (void)
+{
+ GtkSourceBuffer *source_buffer = gtk_source_buffer_new (NULL);
+ GtkTextBuffer *text_buffer = GTK_TEXT_BUFFER (source_buffer);
+ guint occurrences_count;
+
+ gtk_source_buffer_set_search_text (source_buffer, "world\nhello");
+
+ gtk_text_buffer_set_text (text_buffer, "hello world\nhello world\nhello world\n", -1);
+ flush_queue ();
+ occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+ g_assert_cmpuint (occurrences_count, ==, 2);
+
+ gtk_source_buffer_set_search_text (source_buffer, "world\n");
+ flush_queue ();
+ occurrences_count = gtk_source_buffer_get_search_occurrences_count (source_buffer);
+ g_assert_cmpuint (occurrences_count, ==, 3);
+
+ gtk_source_buffer_set_search_text (source_buffer, "\nhello world\n");
+ 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);
+ g_test_add_func ("/Search/occurrences-count/multiple-lines", test_occurrences_count_multiple_lines);
+
+ return g_test_run ();
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]