/*
 * gedit-view-frame.c
 * This file is part of gedit
 *
 * Copyright (C) 2010 - Ignacio Casal Quinteiro
 * Copyright (C) 2013 - Sébastien Wilmet
 *
 * gedit is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * gedit 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with gedit; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "gedit-view-frame.h"
#include "gedit-debug.h"
#include "gedit-utils.h"
#include "libgd/gd.h"

#include <gtksourceview/gtksource.h>
#include <gdk/gdkkeysyms.h>
#include <glib/gi18n.h>
#include <stdlib.h>

#define FLUSH_TIMEOUT_DURATION 30 /* in seconds */

#define SEARCH_POPUP_MARGIN 12

typedef enum
{
	GOTO_LINE,
	SEARCH
} SearchMode;

typedef enum
{
	SEARCH_STATE_NORMAL,
	SEARCH_STATE_NOT_FOUND
} SearchState;

struct _GeditViewFramePrivate
{
	GeditView *view;
	SearchMode search_mode;

	/* Where the search has started. When the user presses escape in the
	 * search entry (to cancel the search), we return to the start_mark.
	 */
	GtkTextMark *start_mark;

	GtkRevealer *revealer;
	GdTaggedEntry *search_entry;
	GdTaggedEntryTag *entry_tag;
	GtkWidget *go_up_button;
	GtkWidget *go_down_button;

	guint flush_timeout_id;
	guint idle_update_entry_tag_id;
	guint remove_entry_tag_timeout_id;
	gulong view_scroll_event_id;
	gulong search_entry_focus_out_id;
	gulong search_entry_changed_id;

	GtkSourceSearchSettings *search_settings;

	/* Used to restore the search state if an incremental search is
	 * cancelled.
	 */
	GtkSourceSearchSettings *old_search_settings;

	/* The original search texts. In search_settings and
	 * old_search_settings, the search text is unescaped. Since the escape
	 * function is not reciprocal, we need to store the original search
	 * texts.
	 */
	gchar *search_text;
	gchar *old_search_text;
};

enum
{
	PROP_0,
	PROP_DOCUMENT,
	PROP_VIEW
};

G_DEFINE_TYPE_WITH_PRIVATE (GeditViewFrame, gedit_view_frame, GTK_TYPE_OVERLAY)

static void
gedit_view_frame_dispose (GObject *object)
{
	GeditViewFrame *frame = GEDIT_VIEW_FRAME (object);

	if (frame->priv->start_mark != NULL && frame->priv->view != NULL)
	{
		GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));

		gtk_text_buffer_delete_mark (buffer, frame->priv->start_mark);
		frame->priv->start_mark = NULL;
	}

	if (frame->priv->flush_timeout_id != 0)
	{
		g_source_remove (frame->priv->flush_timeout_id);
		frame->priv->flush_timeout_id = 0;
	}

	if (frame->priv->idle_update_entry_tag_id != 0)
	{
		g_source_remove (frame->priv->idle_update_entry_tag_id);
		frame->priv->idle_update_entry_tag_id = 0;
	}

	if (frame->priv->remove_entry_tag_timeout_id != 0)
	{
		g_source_remove (frame->priv->remove_entry_tag_timeout_id);
		frame->priv->remove_entry_tag_timeout_id = 0;
	}

	g_clear_object (&frame->priv->entry_tag);
	g_clear_object (&frame->priv->search_settings);
	g_clear_object (&frame->priv->old_search_settings);

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

static void
gedit_view_frame_finalize (GObject *object)
{
	GeditViewFrame *frame = GEDIT_VIEW_FRAME (object);

	g_free (frame->priv->search_text);
	g_free (frame->priv->old_search_text);

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

static void
gedit_view_frame_get_property (GObject    *object,
                               guint       prop_id,
                               GValue     *value,
                               GParamSpec *pspec)
{
	GeditViewFrame *frame = GEDIT_VIEW_FRAME (object);

	switch (prop_id)
	{
		case PROP_DOCUMENT:
			g_value_set_object (value, gedit_view_frame_get_document (frame));
			break;

		case PROP_VIEW:
			g_value_set_object (value, gedit_view_frame_get_view (frame));
			break;

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

static void
hide_search_widget (GeditViewFrame *frame,
                    gboolean        cancel)
{
	GtkTextBuffer *buffer;

	if (!gtk_revealer_get_reveal_child (frame->priv->revealer))
	{
		return;
	}

	if (frame->priv->view_scroll_event_id != 0)
	{
		g_signal_handler_disconnect (frame->priv->view,
		                             frame->priv->view_scroll_event_id);
		frame->priv->view_scroll_event_id = 0;
	}

	if (frame->priv->flush_timeout_id != 0)
	{
		g_source_remove (frame->priv->flush_timeout_id);
		frame->priv->flush_timeout_id = 0;
	}

	gtk_revealer_set_reveal_child (frame->priv->revealer, FALSE);

	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));

	if (cancel && frame->priv->start_mark != NULL)
	{
		GtkTextIter iter;

		gtk_text_buffer_get_iter_at_mark (buffer, &iter,
		                                  frame->priv->start_mark);
		gtk_text_buffer_place_cursor (buffer, &iter);

		gedit_view_scroll_to_cursor (frame->priv->view);
	}

	if (frame->priv->start_mark != NULL)
	{
		gtk_text_buffer_delete_mark (buffer, frame->priv->start_mark);
		frame->priv->start_mark = NULL;
	}

	gtk_widget_grab_focus (GTK_WIDGET (frame->priv->view));
}

static gboolean
search_entry_flush_timeout (GeditViewFrame *frame)
{
	frame->priv->flush_timeout_id = 0;
	hide_search_widget (frame, FALSE);

	return G_SOURCE_REMOVE;
}

static void
renew_flush_timeout (GeditViewFrame *frame)
{
	if (frame->priv->flush_timeout_id != 0)
	{
		g_source_remove (frame->priv->flush_timeout_id);
	}

	frame->priv->flush_timeout_id =
		g_timeout_add_seconds (FLUSH_TIMEOUT_DURATION,
				       (GSourceFunc)search_entry_flush_timeout,
				       frame);
}

static GtkSourceSearchContext *
get_search_context (GeditViewFrame *frame)
{
	GeditDocument *doc;
	GtkSourceSearchContext *search_context;
	GtkSourceSearchSettings *search_settings;

	doc = gedit_view_frame_get_document (frame);
	search_context = gedit_document_get_search_context (doc);

	if (search_context == NULL)
	{
		return NULL;
	}

	search_settings = gtk_source_search_context_get_settings (search_context);

	if (search_settings == frame->priv->search_settings)
	{
		return search_context;
	}

	return NULL;
}

static void
set_search_state (GeditViewFrame *frame,
		  SearchState     state)
{
	GtkStyleContext *context;

	context = gtk_widget_get_style_context (GTK_WIDGET (frame->priv->search_entry));

	if (state == SEARCH_STATE_NOT_FOUND)
	{
		gtk_style_context_add_class (context, "not-found");
	}
	else
	{
		gtk_style_context_remove_class (context, "not-found");
	}
}

static void
finish_search (GeditViewFrame    *frame,
	       gboolean           found)
{
	const gchar *entry_text = gtk_entry_get_text (GTK_ENTRY (frame->priv->search_entry));

	if (found || (entry_text[0] == '\0'))
	{
		gedit_view_scroll_to_cursor (frame->priv->view);

		set_search_state (frame, SEARCH_STATE_NORMAL);
	}
	else
	{
		set_search_state (frame, SEARCH_STATE_NOT_FOUND);
	}
}

static void
start_search_finished (GtkSourceSearchContext *search_context,
		       GAsyncResult           *result,
		       GeditViewFrame         *frame)
{
	GtkTextIter match_start;
	GtkTextIter match_end;
	gboolean found;
	GtkSourceBuffer *buffer;

	found = gtk_source_search_context_forward_finish (search_context,
							  result,
							  &match_start,
							  &match_end,
							  NULL);

	buffer = gtk_source_search_context_get_buffer (search_context);

	if (found)
	{
		gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer),
					      &match_start,
					      &match_end);
	}
	else if (frame->priv->start_mark != NULL)
	{
		GtkTextIter start_at;

		gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer),
						  &start_at,
						  frame->priv->start_mark);

		gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer),
					      &start_at,
					      &start_at);
	}

	finish_search (frame, found);
}

static void
start_search (GeditViewFrame *frame)
{
	GtkTextIter start_at;
	GtkTextBuffer *buffer;
	GtkSourceSearchContext *search_context;

	g_return_if_fail (frame->priv->search_mode == SEARCH);

	search_context = get_search_context (frame);

	if (search_context == NULL)
	{
		return;
	}

	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));

	gtk_text_buffer_get_iter_at_mark (buffer,
					  &start_at,
					  frame->priv->start_mark);

	gtk_source_search_context_forward_async (search_context,
						 &start_at,
						 NULL,
						 (GAsyncReadyCallback)start_search_finished,
						 frame);
}

static void
forward_search_finished (GtkSourceSearchContext *search_context,
			 GAsyncResult           *result,
			 GeditViewFrame         *frame)
{
	GtkTextIter match_start;
	GtkTextIter match_end;
	gboolean found;

	found = gtk_source_search_context_forward_finish (search_context,
							  result,
							  &match_start,
							  &match_end,
							  NULL);

	if (found)
	{
		GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));

		gtk_text_buffer_select_range (buffer,
					      &match_start,
					      &match_end);
	}

	finish_search (frame, found);
}

static void
forward_search (GeditViewFrame *frame)
{
	GtkTextIter start_at;
	GtkTextBuffer *buffer;
	GtkSourceSearchContext *search_context;

	g_return_if_fail (frame->priv->search_mode == SEARCH);

	search_context = get_search_context (frame);

	if (search_context == NULL)
	{
		return;
	}

	renew_flush_timeout (frame);

	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));

	gtk_text_buffer_get_selection_bounds (buffer, NULL, &start_at);

	gtk_source_search_context_forward_async (search_context,
						 &start_at,
						 NULL,
						 (GAsyncReadyCallback)forward_search_finished,
						 frame);
}

static void
backward_search_finished (GtkSourceSearchContext *search_context,
			  GAsyncResult           *result,
			  GeditViewFrame         *frame)
{
	GtkTextIter match_start;
	GtkTextIter match_end;
	gboolean found;
	GtkSourceBuffer *buffer;

	found = gtk_source_search_context_backward_finish (search_context,
							   result,
							   &match_start,
							   &match_end,
							   NULL);

	buffer = gtk_source_search_context_get_buffer (search_context);

	if (found)
	{
		gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer),
					      &match_start,
					      &match_end);
	}

	finish_search (frame, found);
}

static void
backward_search (GeditViewFrame *frame)
{
	GtkTextIter start_at;
	GtkTextBuffer *buffer;
	GtkSourceSearchContext *search_context;

	g_return_if_fail (frame->priv->search_mode == SEARCH);

	search_context = get_search_context (frame);

	if (search_context == NULL)
	{
		return;
	}

	renew_flush_timeout (frame);

	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));

	gtk_text_buffer_get_selection_bounds (buffer, &start_at, NULL);

	gtk_source_search_context_backward_async (search_context,
						  &start_at,
						  NULL,
						  (GAsyncReadyCallback)backward_search_finished,
						  frame);
}

static gboolean
search_widget_scroll_event (GtkWidget      *widget,
                            GdkEventScroll *event,
                            GeditViewFrame *frame)
{
	if (frame->priv->search_mode == GOTO_LINE)
	{
		return GDK_EVENT_PROPAGATE;
	}

	if (event->state & GDK_CONTROL_MASK)
	{
		if (event->direction == GDK_SCROLL_UP)
		{
			backward_search (frame);
			return GDK_EVENT_STOP;
		}
		else if (event->direction == GDK_SCROLL_DOWN)
		{
			forward_search (frame);
			return GDK_EVENT_STOP;
		}
	}

	return GDK_EVENT_PROPAGATE;
}

static GtkSourceSearchSettings *
copy_search_settings (GtkSourceSearchSettings *settings)
{
	GtkSourceSearchSettings *new_settings = gtk_source_search_settings_new ();
	gboolean val;
	const gchar *text;

	if (settings == NULL)
	{
		return new_settings;
	}

	val = gtk_source_search_settings_get_case_sensitive (settings);
	gtk_source_search_settings_set_case_sensitive (new_settings, val);

	val = gtk_source_search_settings_get_wrap_around (settings);
	gtk_source_search_settings_set_wrap_around (new_settings, val);

	val = gtk_source_search_settings_get_at_word_boundaries (settings);
	gtk_source_search_settings_set_at_word_boundaries (new_settings, val);

	val = gtk_source_search_settings_get_regex_enabled (settings);
	gtk_source_search_settings_set_regex_enabled (new_settings, val);

	text = gtk_source_search_settings_get_search_text (settings);
	gtk_source_search_settings_set_search_text (new_settings, text);

	return new_settings;
}

static gboolean
search_widget_key_press_event (GtkWidget      *widget,
                               GdkEventKey    *event,
                               GeditViewFrame *frame)
{
	guint modifiers = gtk_accelerator_get_default_mod_mask ();

	/* Close window */
	if (event->keyval == GDK_KEY_Tab)
	{
		hide_search_widget (frame, FALSE);
		return GDK_EVENT_STOP;
	}

	/* Close window and cancel the search */
	if (event->keyval == GDK_KEY_Escape)
	{
		GtkSourceSearchContext *search_context = get_search_context (frame);

		if (frame->priv->search_mode == SEARCH &&
		    search_context != NULL)
		{
			g_clear_object (&frame->priv->search_settings);
			frame->priv->search_settings = copy_search_settings (frame->priv->old_search_settings);

			gtk_source_search_context_set_settings (search_context,
								frame->priv->search_settings);

			g_free (frame->priv->search_text);
			frame->priv->search_text = NULL;

			if (frame->priv->old_search_text != NULL)
			{
				frame->priv->search_text = g_strdup (frame->priv->old_search_text);
			}
		}

		hide_search_widget (frame, TRUE);
		return GDK_EVENT_STOP;
	}

	if (frame->priv->search_mode == GOTO_LINE)
	{
		return GDK_EVENT_PROPAGATE;
	}

	/* SEARCH mode */

	/* select previous matching iter */
	if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up)
	{
		backward_search (frame);
		return GDK_EVENT_STOP;
	}

	if (((event->state & modifiers) == (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) &&
	    (event->keyval == GDK_KEY_g || event->keyval == GDK_KEY_G))
	{
		backward_search (frame);
		return GDK_EVENT_STOP;
	}

	/* select next matching iter */
	if (event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down)
	{
		forward_search (frame);
		return GDK_EVENT_STOP;
	}

	if (((event->state & modifiers) == GDK_CONTROL_MASK) &&
	    (event->keyval == GDK_KEY_g || event->keyval == GDK_KEY_G))
	{
		forward_search (frame);
		return GDK_EVENT_STOP;
	}

	return GDK_EVENT_PROPAGATE;
}

static gboolean
remove_entry_tag_timeout_cb (GeditViewFrame *frame)
{
	frame->priv->remove_entry_tag_timeout_id = 0;

	gd_tagged_entry_remove_tag (frame->priv->search_entry,
				    frame->priv->entry_tag);

	return G_SOURCE_REMOVE;
}

static void
update_entry_tag (GeditViewFrame *frame)
{
	GtkSourceSearchContext *search_context;
	GtkTextBuffer *buffer;
	GtkTextIter select_start;
	GtkTextIter select_end;
	gint count;
	gint pos;
	gchar *label;

	if (frame->priv->search_mode == GOTO_LINE)
	{
		gd_tagged_entry_remove_tag (frame->priv->search_entry,
					    frame->priv->entry_tag);
		return;
	}

	search_context = get_search_context (frame);

	if (search_context == NULL)
	{
		return;
	}

	count = gtk_source_search_context_get_occurrences_count (search_context);

	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));
	gtk_text_buffer_get_selection_bounds (buffer, &select_start, &select_end);

	pos = gtk_source_search_context_get_occurrence_position (search_context,
								 &select_start,
								 &select_end);

	if (count == -1 || pos == -1)
	{
		/* The buffer is not fully scanned. Remove the tag after a short
		 * delay. If we don't remove the tag at all, the information can
		 * be outdated during a too long time (for big buffers). And if
		 * the tag is removed directly, there is some flashing for small
		 * buffers: the tag disappears and reappear after a really short
		 * time.
		 */

		if (frame->priv->remove_entry_tag_timeout_id == 0)
		{
			frame->priv->remove_entry_tag_timeout_id =
				g_timeout_add (500,
					       (GSourceFunc)remove_entry_tag_timeout_cb,
					       frame);
		}

		return;
	}

	if (count == 0 || pos == 0)
	{
		gd_tagged_entry_remove_tag (frame->priv->search_entry,
					    frame->priv->entry_tag);
		return;
	}

	if (frame->priv->remove_entry_tag_timeout_id != 0)
	{
		g_source_remove (frame->priv->remove_entry_tag_timeout_id);
		frame->priv->remove_entry_tag_timeout_id = 0;
	}

	/* Translators: the first %d is the position of the current search
	 * occurrence, and the second %d is the total number of search
	 * occurrences.
	 */
	label = g_strdup_printf (_("%d of %d"), pos, count);

	gd_tagged_entry_tag_set_label (frame->priv->entry_tag, label);

	gd_tagged_entry_add_tag (frame->priv->search_entry,
				 frame->priv->entry_tag);

	g_free (label);
}

static gboolean
update_entry_tag_idle_cb (GeditViewFrame *frame)
{
	frame->priv->idle_update_entry_tag_id = 0;

	update_entry_tag (frame);

	return G_SOURCE_REMOVE;
}

static void
install_update_entry_tag_idle (GeditViewFrame *frame)
{
	if (frame->priv->idle_update_entry_tag_id == 0)
	{
		frame->priv->idle_update_entry_tag_id = g_idle_add ((GSourceFunc)update_entry_tag_idle_cb,
								    frame);
	}
}

static void
update_search_text (GeditViewFrame *frame)
{
	const gchar *entry_text = gtk_entry_get_text (GTK_ENTRY (frame->priv->search_entry));

	g_free (frame->priv->search_text);
	frame->priv->search_text = g_strdup (entry_text);

	if (gtk_source_search_settings_get_regex_enabled (frame->priv->search_settings))
	{
		gtk_source_search_settings_set_search_text (frame->priv->search_settings,
							    entry_text);
	}
	else
	{
		gchar *unescaped_entry_text = gtk_source_utils_unescape_search_text (entry_text);

		gtk_source_search_settings_set_search_text (frame->priv->search_settings,
							    unescaped_entry_text);

		g_free (unescaped_entry_text);
	}
}

static void
regex_toggled_cb (GtkCheckMenuItem *menu_item,
		  GeditViewFrame   *frame)
{
	gtk_source_search_settings_set_regex_enabled (frame->priv->search_settings,
						      gtk_check_menu_item_get_active (menu_item));

	start_search (frame);
}

static void
at_word_boundaries_toggled_cb (GtkCheckMenuItem *menu_item,
			       GeditViewFrame   *frame)
{
	gtk_source_search_settings_set_at_word_boundaries (frame->priv->search_settings,
							   gtk_check_menu_item_get_active (menu_item));

	start_search (frame);
}

static void
case_sensitive_toggled_cb (GtkCheckMenuItem *menu_item,
			   GeditViewFrame   *frame)
{
	gtk_source_search_settings_set_case_sensitive (frame->priv->search_settings,
						       gtk_check_menu_item_get_active (menu_item));

	start_search (frame);
}

static void
add_popup_menu_items (GeditViewFrame *frame,
		      GtkWidget      *menu)
{
	GtkWidget *menu_item;
	gboolean val;

	/* create "Wrap Around" menu item. */
	menu_item = gtk_check_menu_item_new_with_mnemonic (_("_Wrap Around"));

	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
	gtk_widget_show (menu_item);

	g_object_bind_property (frame->priv->search_settings, "wrap-around",
				menu_item, "active",
				G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);

	/* create "Match as Regular Expression" menu item. */
	menu_item = gtk_check_menu_item_new_with_mnemonic (_("Match as _Regular Expression"));

	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
	gtk_widget_show (menu_item);

	val = gtk_source_search_settings_get_regex_enabled (frame->priv->search_settings);
	gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), val);

	g_signal_connect (menu_item,
			  "toggled",
			  G_CALLBACK (regex_toggled_cb),
			  frame);

	/* create "Match Entire Word Only" menu item. */
	menu_item = gtk_check_menu_item_new_with_mnemonic (_("Match _Entire Word Only"));

	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
	gtk_widget_show (menu_item);

	val = gtk_source_search_settings_get_at_word_boundaries (frame->priv->search_settings);
	gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), val);

	g_signal_connect (menu_item,
			  "toggled",
			  G_CALLBACK (at_word_boundaries_toggled_cb),
			  frame);

	/* create "Match Case" menu item. */
	menu_item = gtk_check_menu_item_new_with_mnemonic (_("_Match Case"));

	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
	gtk_widget_show (menu_item);

	val = gtk_source_search_settings_get_case_sensitive (frame->priv->search_settings);
	gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), val);

	g_signal_connect (menu_item,
			  "toggled",
			  G_CALLBACK (case_sensitive_toggled_cb),
			  frame);
}

static void
popup_menu_hide_cb (GeditViewFrame *frame)
{
	renew_flush_timeout (frame);

	g_signal_handler_unblock (frame->priv->search_entry,
				  frame->priv->search_entry_focus_out_id);
}

static void
setup_popup_menu (GeditViewFrame *frame,
		  GtkWidget      *menu)
{
	if (frame->priv->flush_timeout_id != 0)
	{
		g_source_remove (frame->priv->flush_timeout_id);
		frame->priv->flush_timeout_id = 0;
	}

	g_signal_handler_block (frame->priv->search_entry,
				frame->priv->search_entry_focus_out_id);

	g_signal_connect_swapped (menu,
				  "hide",
				  G_CALLBACK (popup_menu_hide_cb),
				  frame);
}

static void
search_entry_populate_popup (GtkEntry       *entry,
                             GtkMenu        *menu,
                             GeditViewFrame *frame)
{
	GtkWidget *menu_item;

	if (frame->priv->search_mode == GOTO_LINE)
	{
		return;
	}

	setup_popup_menu (frame, GTK_WIDGET (menu));

	/* separator */
	menu_item = gtk_separator_menu_item_new ();
	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
	gtk_widget_show (menu_item);

	add_popup_menu_items (frame, GTK_WIDGET (menu));
}

static void
search_entry_icon_release (GtkEntry             *entry,
                           GtkEntryIconPosition  icon_pos,
                           GdkEventButton       *event,
                           GeditViewFrame       *frame)
{
	GtkWidget *menu;

	if (frame->priv->search_mode == GOTO_LINE ||
	    icon_pos != GTK_ENTRY_ICON_PRIMARY)
	{
		return;
	}

	menu = gtk_menu_new ();
	gtk_widget_show (menu);

	setup_popup_menu (frame, menu);
	add_popup_menu_items (frame, menu);

	gtk_menu_popup (GTK_MENU (menu),
	                NULL, NULL,
	                gedit_utils_menu_position_under_widget, entry,
	                event->button, event->time);
}

static void
search_entry_activate (GtkEntry       *entry,
                       GeditViewFrame *frame)
{
	hide_search_widget (frame, FALSE);
}

static void
search_entry_insert_text (GtkEditable    *editable,
                          const gchar    *text,
                          gint            length,
                          gint           *position,
                          GeditViewFrame *frame)
{
	gunichar c;
	const gchar *p;
	const gchar *end;
	const gchar *next;

	if (frame->priv->search_mode == SEARCH)
	{
		return;
	}

	p = text;
	end = text + length;

	if (p == end)
	{
		return;
	}

	c = g_utf8_get_char (p);

	if (((c == '-' || c == '+') && *position == 0) ||
	    (c == ':' && *position != 0))
	{
		gchar *s = NULL;

		if (c == ':')
		{
			s = gtk_editable_get_chars (editable, 0, -1);
			s = g_utf8_strchr (s, -1, ':');
		}

		if (s == NULL || s == p)
		{
			next = g_utf8_next_char (p);
			p = next;
		}

		g_free (s);
	}

	while (p != end)
	{
		next = g_utf8_next_char (p);

		c = g_utf8_get_char (p);

		if (!g_unichar_isdigit (c))
		{
			g_signal_stop_emission_by_name (editable, "insert_text");
			gtk_widget_error_bell (GTK_WIDGET (frame->priv->search_entry));
			break;
		}

		p = next;
	}
}

static void
customize_for_search_mode (GeditViewFrame *frame)
{
	GIcon *icon;
	gint width_request;

	if (frame->priv->search_mode == SEARCH)
	{
		icon = g_themed_icon_new_with_default_fallbacks ("edit-find-symbolic");

		width_request = 260;

		gtk_widget_set_tooltip_text (GTK_WIDGET (frame->priv->search_entry),
		                             _("String you want to search for"));

		gtk_widget_show (frame->priv->go_up_button);
		gtk_widget_show (frame->priv->go_down_button);
	}
	else
	{
		icon = g_themed_icon_new_with_default_fallbacks ("go-jump-symbolic");

		width_request = 160;

		gtk_widget_set_tooltip_text (GTK_WIDGET (frame->priv->search_entry),
		                             _("Line you want to move the cursor to"));

		gtk_widget_hide (frame->priv->go_up_button);
		gtk_widget_hide (frame->priv->go_down_button);
	}

	gtk_entry_set_icon_from_gicon (GTK_ENTRY (frame->priv->search_entry),
	                               GTK_ENTRY_ICON_PRIMARY,
	                               icon);

	gtk_widget_set_size_request (GTK_WIDGET (frame->priv->search_entry),
				     width_request,
				     -1);

	g_object_unref (icon);
}

static void
update_goto_line (GeditViewFrame *frame)
{
	const gchar *entry_text;
	gboolean moved;
	gboolean moved_offset;
	gint line;
	gint offset_line = 0;
	gint line_offset = 0;
	gchar **split_text = NULL;
	const gchar *text;
	GtkTextIter iter;
	GeditDocument *doc;

	entry_text = gtk_entry_get_text (GTK_ENTRY (frame->priv->search_entry));

	if (entry_text[0] == '\0')
	{
		return;
	}

	doc = gedit_view_frame_get_document (frame);

	gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (doc),
					  &iter,
					  frame->priv->start_mark);

	split_text = g_strsplit (entry_text, ":", -1);

	if (g_strv_length (split_text) > 1)
	{
		text = split_text[0];
	}
	else
	{
		text = entry_text;
	}

	if (text[0] == '-')
	{
		gint cur_line = gtk_text_iter_get_line (&iter);

		if (text[1] != '\0')
		{
			offset_line = MAX (atoi (text + 1), 0);
		}

		line = MAX (cur_line - offset_line, 0);
	}
	else if (entry_text[0] == '+')
	{
		gint cur_line = gtk_text_iter_get_line (&iter);

		if (text[1] != '\0')
		{
			offset_line = MAX (atoi (text + 1), 0);
		}

		line = cur_line + offset_line;
	}
	else
	{
		line = MAX (atoi (text) - 1, 0);
	}

	if (split_text[1] != NULL)
	{
		line_offset = atoi (split_text[1]);
	}

	g_strfreev (split_text);

	moved = gedit_document_goto_line (doc, line);
	moved_offset = gedit_document_goto_line_offset (doc, line, line_offset);

	gedit_view_scroll_to_cursor (frame->priv->view);

	if (!moved || !moved_offset)
	{
		set_search_state (frame, SEARCH_STATE_NOT_FOUND);
	}
	else
	{
		set_search_state (frame, SEARCH_STATE_NORMAL);
	}
}

static void
search_entry_changed_cb (GtkEntry       *entry,
			 GeditViewFrame *frame)
{
	renew_flush_timeout (frame);

	if (frame->priv->search_mode == SEARCH)
	{
		update_search_text (frame);
		start_search (frame);
	}
	else
	{
		update_goto_line (frame);
	}
}

static gboolean
search_entry_focus_out_event (GtkWidget      *widget,
                              GdkEventFocus  *event,
                              GeditViewFrame *frame)
{
	hide_search_widget (frame, FALSE);
	return GDK_EVENT_PROPAGATE;
}

static void
mark_set_cb (GtkTextBuffer  *buffer,
	     GtkTextIter    *location,
	     GtkTextMark    *mark,
	     GeditViewFrame *frame)
{
	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)
	{
		install_update_entry_tag_idle (frame);
	}
}

static gboolean
get_selected_text (GtkTextBuffer  *doc,
		   gchar         **selected_text,
		   gint           *len)
{
	GtkTextIter start;
	GtkTextIter end;

	g_return_val_if_fail (selected_text != NULL, FALSE);
	g_return_val_if_fail (*selected_text == NULL, FALSE);

	if (!gtk_text_buffer_get_selection_bounds (doc, &start, &end))
	{
		if (len != NULL)
		{
			*len = 0;
		}

		return FALSE;
	}

	*selected_text = gtk_text_buffer_get_slice (doc, &start, &end, TRUE);

	if (len != NULL)
	{
		*len = g_utf8_strlen (*selected_text, -1);
	}

	return TRUE;
}

static void
init_search_entry (GeditViewFrame *frame)
{
	GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));

	if (frame->priv->search_mode == GOTO_LINE)
	{
		gint line;
		gchar *line_str;
		GtkTextIter iter;

		gtk_text_buffer_get_iter_at_mark (buffer,
		                                  &iter,
		                                  frame->priv->start_mark);

		line = gtk_text_iter_get_line (&iter);

		line_str = g_strdup_printf ("%d", line + 1);

		gtk_entry_set_text (GTK_ENTRY (frame->priv->search_entry), line_str);

		gtk_editable_select_region (GTK_EDITABLE (frame->priv->search_entry),
		                            0, -1);

		g_free (line_str);
	}
	else
	{
		/* SEARCH mode */
		gboolean selection_exists;
		gchar *search_text = NULL;
		gint selection_len = 0;
		GtkSourceSearchContext *search_context;

		if (frame->priv->search_settings == NULL)
		{
			frame->priv->search_settings = gtk_source_search_settings_new ();
			gtk_source_search_settings_set_wrap_around (frame->priv->search_settings, TRUE);
		}

		g_clear_object (&frame->priv->old_search_settings);
		frame->priv->old_search_settings = copy_search_settings (frame->priv->search_settings);

		g_free (frame->priv->old_search_text);
		frame->priv->old_search_text = NULL;

		if (frame->priv->search_text != NULL)
		{
			frame->priv->old_search_text = g_strdup (frame->priv->search_text);
		}

		search_context = get_search_context (frame);

		if (search_context == NULL)
		{
			search_context = gtk_source_search_context_new (GTK_SOURCE_BUFFER (buffer),
									frame->priv->search_settings);

			gedit_document_set_search_context (GEDIT_DOCUMENT (buffer),
							   search_context);

			g_signal_connect_swapped (search_context,
						  "notify::occurrences-count",
						  G_CALLBACK (install_update_entry_tag_idle),
						  frame);

			g_object_unref (search_context);
		}

		selection_exists = get_selected_text (buffer,
		                                      &search_text,
		                                      &selection_len);

		if (selection_exists && (search_text != NULL) && (selection_len <= 160))
		{
			gchar *search_text_escaped;

			if (gtk_source_search_settings_get_regex_enabled (frame->priv->search_settings))
			{
				search_text_escaped = g_regex_escape_string (search_text, -1);
			}
			else
			{
				search_text_escaped = gtk_source_utils_escape_search_text (search_text);
			}

			if (g_strcmp0 (search_text_escaped, frame->priv->search_text) == 0)
			{
				/* The search text is the same, no need to
				 * trigger the search again. We prefer to select
				 * the text in the search entry, so the user can
				 * easily search something else.
				 */
				g_signal_handler_block (frame->priv->search_entry,
							frame->priv->search_entry_changed_id);

				gtk_entry_set_text (GTK_ENTRY (frame->priv->search_entry),
						    search_text_escaped);

				gtk_editable_select_region (GTK_EDITABLE (frame->priv->search_entry),
							    0, -1);

				g_signal_handler_unblock (frame->priv->search_entry,
							  frame->priv->search_entry_changed_id);
			}
			else
			{
				/* search_text_escaped is new, so we trigger the
				 * search (by not blocking the signal), and we
				 * don't select the text in the search entry
				 * because the user wants to search for
				 * search_text_escaped, not for something else.
				 */
				gtk_entry_set_text (GTK_ENTRY (frame->priv->search_entry),
						    search_text_escaped);

				gtk_editable_set_position (GTK_EDITABLE (frame->priv->search_entry), -1);
			}

			g_free (search_text_escaped);
		}
		else if (frame->priv->search_text != NULL)
		{
			g_signal_handler_block (frame->priv->search_entry,
			                        frame->priv->search_entry_changed_id);

			gtk_entry_set_text (GTK_ENTRY (frame->priv->search_entry),
					    frame->priv->search_text);

			gtk_editable_select_region (GTK_EDITABLE (frame->priv->search_entry),
			                            0, -1);

			g_signal_handler_unblock (frame->priv->search_entry,
			                          frame->priv->search_entry_changed_id);
		}

		g_free (search_text);
	}
}

static void
start_interactive_search_real (GeditViewFrame *frame,
			       SearchMode      request_search_mode)
{
	GtkTextBuffer *buffer;
	GtkTextIter iter;

	if (gtk_revealer_get_reveal_child (frame->priv->revealer))
	{
		if (frame->priv->search_mode != request_search_mode)
		{
			hide_search_widget (frame, TRUE);
		}
		else
		{
			gtk_editable_select_region (GTK_EDITABLE (frame->priv->search_entry),
			                            0, -1);
			return;
		}
	}

	frame->priv->search_mode = request_search_mode;

	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view));

	if (frame->priv->search_mode == SEARCH)
	{
		gtk_text_buffer_get_selection_bounds (buffer, &iter, NULL);
	}
	else
	{
		GtkTextMark *mark = gtk_text_buffer_get_insert (buffer);
		gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
	}

	if (frame->priv->start_mark != NULL)
	{
		gtk_text_buffer_delete_mark (buffer, frame->priv->start_mark);
	}

	frame->priv->start_mark = gtk_text_buffer_create_mark (buffer, NULL, &iter, FALSE);

	gtk_revealer_set_reveal_child (frame->priv->revealer, TRUE);

	/* NOTE: we must be very careful here to not have any text before
	   focusing the entry because when the entry is focused the text is
	   selected, and gtk+ doesn't allow us to have more than one selection
	   active */
	g_signal_handler_block (frame->priv->search_entry,
	                        frame->priv->search_entry_changed_id);

	gtk_entry_set_text (GTK_ENTRY (frame->priv->search_entry), "");

	g_signal_handler_unblock (frame->priv->search_entry,
	                          frame->priv->search_entry_changed_id);

	gtk_widget_grab_focus (GTK_WIDGET (frame->priv->search_entry));

	customize_for_search_mode (frame);
	init_search_entry (frame);

	/* Manage the scroll also for the view */
	frame->priv->view_scroll_event_id =
		g_signal_connect (frame->priv->view, "scroll-event",
			          G_CALLBACK (search_widget_scroll_event),
			          frame);

	renew_flush_timeout (frame);

	install_update_entry_tag_idle (frame);
}

static void
gedit_view_frame_class_init (GeditViewFrameClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

	object_class->dispose = gedit_view_frame_dispose;
	object_class->finalize = gedit_view_frame_finalize;
	object_class->get_property = gedit_view_frame_get_property;

	g_object_class_install_property (object_class, PROP_DOCUMENT,
	                                 g_param_spec_object ("document",
	                                                      "Document",
	                                                      "The Document",
	                                                      GEDIT_TYPE_DOCUMENT,
	                                                      G_PARAM_READABLE |
	                                                      G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (object_class, PROP_VIEW,
	                                 g_param_spec_object ("view",
	                                                      "View",
	                                                      "The View",
	                                                      GEDIT_TYPE_VIEW,
	                                                      G_PARAM_READABLE |
	                                                      G_PARAM_STATIC_STRINGS));

	/* Bind class to template */
	gtk_widget_class_set_template_from_resource (widget_class,
	                                             "/org/gnome/gedit/ui/gedit-view-frame.ui");
	gtk_widget_class_bind_template_child_private (widget_class, GeditViewFrame, view);
	gtk_widget_class_bind_template_child_private (widget_class, GeditViewFrame, revealer);
	gtk_widget_class_bind_template_child_private (widget_class, GeditViewFrame, search_entry);
	gtk_widget_class_bind_template_child_private (widget_class, GeditViewFrame, go_up_button);
	gtk_widget_class_bind_template_child_private (widget_class, GeditViewFrame, go_down_button);
}

static GMountOperation *
view_frame_mount_operation_factory (GeditDocument *doc,
				    gpointer       user_data)
{
	GtkWidget *frame = user_data;
	GtkWidget *window = gtk_widget_get_toplevel (frame);

	return gtk_mount_operation_new (GTK_WINDOW (window));
}

static void
gedit_view_frame_init (GeditViewFrame *frame)
{
	GeditDocument *doc;
	GdkRGBA transparent = {0, 0, 0, 0};

	frame->priv = gedit_view_frame_get_instance_private (frame);

	gtk_widget_init_template (GTK_WIDGET (frame));

	gtk_widget_override_background_color (GTK_WIDGET (frame), 0, &transparent);

	doc = gedit_view_frame_get_document (frame);
	_gedit_document_set_mount_operation_factory (doc,
						     view_frame_mount_operation_factory,
						     frame);

	frame->priv->entry_tag = gd_tagged_entry_tag_new ("");

	gd_tagged_entry_tag_set_style (frame->priv->entry_tag,
				       "gedit-search-entry-occurrences-tag");

	gd_tagged_entry_tag_set_has_close_button (frame->priv->entry_tag, FALSE);

	if (gtk_widget_get_direction (GTK_WIDGET (frame->priv->revealer)) == GTK_TEXT_DIR_LTR)
	{
		gtk_widget_set_margin_right (GTK_WIDGET (frame->priv->revealer),
					     SEARCH_POPUP_MARGIN);
	}
	else
	{
		gtk_widget_set_margin_left (GTK_WIDGET (frame->priv->revealer),
					    SEARCH_POPUP_MARGIN);
	}

	g_signal_connect (doc,
			  "mark-set",
			  G_CALLBACK (mark_set_cb),
			  frame);

	g_signal_connect (frame->priv->revealer,
			  "key-press-event",
	                  G_CALLBACK (search_widget_key_press_event),
	                  frame);

	g_signal_connect (frame->priv->revealer,
			  "scroll-event",
	                  G_CALLBACK (search_widget_scroll_event),
	                  frame);

	g_signal_connect (frame->priv->search_entry,
			  "populate-popup",
	                  G_CALLBACK (search_entry_populate_popup),
	                  frame);

	g_signal_connect (frame->priv->search_entry,
			  "icon-release",
	                  G_CALLBACK (search_entry_icon_release),
	                  frame);

	g_signal_connect (frame->priv->search_entry,
			  "activate",
	                  G_CALLBACK (search_entry_activate),
	                  frame);

	g_signal_connect (frame->priv->search_entry,
			  "insert-text",
	                  G_CALLBACK (search_entry_insert_text),
	                  frame);

	frame->priv->search_entry_changed_id =
		g_signal_connect (frame->priv->search_entry,
				  "changed",
		                  G_CALLBACK (search_entry_changed_cb),
		                  frame);

	frame->priv->search_entry_focus_out_id =
		g_signal_connect (frame->priv->search_entry,
				  "focus-out-event",
				  G_CALLBACK (search_entry_focus_out_event),
				  frame);

	g_signal_connect_swapped (frame->priv->go_up_button,
				  "clicked",
				  G_CALLBACK (backward_search),
				  frame);

	g_signal_connect_swapped (frame->priv->go_down_button,
				  "clicked",
				  G_CALLBACK (forward_search),
				  frame);
}

GeditViewFrame *
gedit_view_frame_new ()
{
	return g_object_new (GEDIT_TYPE_VIEW_FRAME, NULL);
}

GeditDocument *
gedit_view_frame_get_document (GeditViewFrame *frame)
{
	g_return_val_if_fail (GEDIT_IS_VIEW_FRAME (frame), NULL);

	return GEDIT_DOCUMENT (gtk_text_view_get_buffer (GTK_TEXT_VIEW (frame->priv->view)));
}

GeditView *
gedit_view_frame_get_view (GeditViewFrame *frame)
{
	g_return_val_if_fail (GEDIT_IS_VIEW_FRAME (frame), NULL);

	return frame->priv->view;
}

void
gedit_view_frame_popup_search (GeditViewFrame *frame)
{
	g_return_if_fail (GEDIT_IS_VIEW_FRAME (frame));

	start_interactive_search_real (frame, SEARCH);
}

void
gedit_view_frame_popup_goto_line (GeditViewFrame *frame)
{
	g_return_if_fail (GEDIT_IS_VIEW_FRAME (frame));

	start_interactive_search_real (frame, GOTO_LINE);
}

void
gedit_view_frame_clear_search (GeditViewFrame *frame)
{
	g_return_if_fail (GEDIT_IS_VIEW_FRAME (frame));

	g_signal_handler_block (frame->priv->search_entry,
	                        frame->priv->search_entry_changed_id);

	gtk_entry_set_text (GTK_ENTRY (frame->priv->search_entry), "");

	g_signal_handler_unblock (frame->priv->search_entry,
	                          frame->priv->search_entry_changed_id);

	gtk_widget_grab_focus (GTK_WIDGET (frame->priv->view));
}
