/* ide-editor-page.c
 *
 * Copyright 2017-2019 Christian Hergert <chergert@redhat.com>
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "ide-editor-page"

#include "config.h"

#include <dazzle.h>
#include <libpeas/peas.h>
#include <gtksourceview/gtksource.h>
#include <pango/pangofc-fontmap.h>

#include "ide-editor-page.h"
#include "ide-editor-page-addin.h"
#include "ide-editor-private.h"
#include "ide-line-change-gutter-renderer.h"

#define AUTO_HIDE_TIMEOUT_SECONDS 5

enum {
  PROP_0,
  PROP_AUTO_HIDE_MAP,
  PROP_BUFFER,
  PROP_BUFFER_FILE,
  PROP_SEARCH,
  PROP_SHOW_MAP,
  PROP_VIEW,
  N_PROPS
};

static void ide_editor_page_update_reveal_timer (IdeEditorPage *self);

G_DEFINE_FINAL_TYPE (IdeEditorPage, ide_editor_page, IDE_TYPE_PAGE)

DZL_DEFINE_COUNTER (instances, "Editor", "N Views", "Number of editor views");

static GParamSpec *properties [N_PROPS];
static FcConfig *localFontConfig;

static void
ide_editor_page_load_fonts (IdeEditorPage *self)
{
  PangoFontMap *font_map;
  PangoFontDescription *font_desc;

  if (g_once_init_enter (&localFontConfig))
    {
      const gchar *font_path = PACKAGE_DATADIR "/gnome-builder/fonts/BuilderBlocks.ttf";
      FcConfig *config = FcInitLoadConfigAndFonts ();

      if (g_getenv ("GB_IN_TREE_FONTS") != NULL)
        font_path = "data/fonts/BuilderBlocks.ttf";

      if (!g_file_test (font_path, G_FILE_TEST_IS_REGULAR))
        g_warning ("Failed to locate \"%s\"", font_path);

      FcConfigAppFontAddFile (config, (const FcChar8 *)font_path);

      g_once_init_leave (&localFontConfig, config);
    }

  font_map = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT);
  pango_fc_font_map_set_config (PANGO_FC_FONT_MAP (font_map), localFontConfig);
  gtk_widget_set_font_map (GTK_WIDGET (self->map), font_map);
  font_desc = pango_font_description_from_string ("BuilderBlocks");
  pango_font_description_set_absolute_size (font_desc, (96.0/72.0) * 2 * PANGO_SCALE);

  g_assert (localFontConfig != NULL);
  g_assert (font_map != NULL);
  g_assert (font_desc != NULL);

  g_object_set (self->map, "font-desc", font_desc, NULL);

  pango_font_description_free (font_desc);
  g_object_unref (font_map);
}

static void
ide_editor_page_update_icon (IdeEditorPage *self)
{
  g_autofree gchar *name = NULL;
  g_autofree gchar *content_type = NULL;
  g_autofree gchar *sniff = NULL;
  g_autoptr(GIcon) icon = NULL;
  GtkTextIter begin, end;
  GFile *file;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_BUFFER (self->buffer));

  /* Get first 1024 bytes to help determine content type */
  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end);
  if (gtk_text_iter_get_offset (&end) > 1024)
    gtk_text_iter_set_offset (&end, 1024);
  sniff = gtk_text_iter_get_slice (&begin, &end);

  /* Now get basename for content type */
  file = ide_buffer_get_file (self->buffer);
  name = g_file_get_basename (file);

  /* Guess content type */
  content_type = g_content_type_guess (name, (const guchar *)sniff, strlen (sniff), NULL);

  /* Update icon to match guess */
  icon = ide_g_content_type_get_symbolic_icon (content_type, name);
  ide_page_set_icon (IDE_PAGE (self), icon);
}

static void
ide_editor_page_buffer_notify_failed (IdeEditorPage *self,
                                      GParamSpec    *pspec,
                                      IdeBuffer     *buffer)
{
  gboolean failed;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_BUFFER (buffer));

  failed = ide_buffer_get_failed (buffer);

  ide_page_set_failed (IDE_PAGE (self), failed);
}

static void
ide_editor_page_stop_search (IdeEditorPage      *self,
                             IdeEditorSearchBar *search_bar)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_EDITOR_SEARCH_BAR (search_bar));

  gtk_revealer_set_reveal_child (self->search_revealer, FALSE);
  gtk_widget_grab_focus (GTK_WIDGET (self->source_view));
}

static void
ide_editor_page_notify_child_revealed (IdeEditorPage *self,
                                       GParamSpec    *pspec,
                                       GtkRevealer   *revealer)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (GTK_IS_REVEALER (revealer));

  if (gtk_revealer_get_child_revealed (revealer))
    {
      GtkWidget *toplevel = gtk_widget_get_ancestor (GTK_WIDGET (revealer), GTK_TYPE_WINDOW);
      GtkWidget *focus = gtk_window_get_focus (GTK_WINDOW (toplevel));

      /* Only focus the search bar if it doesn't already have focus,
       * as it can reselect the search text.
       */
      if (focus == NULL || !gtk_widget_is_ancestor (focus, GTK_WIDGET (revealer)))
        gtk_widget_grab_focus (GTK_WIDGET (self->search_bar));
    }
}

static gboolean
ide_editor_page_focus_in_event (IdeEditorPage *self,
                                GdkEventFocus *focus,
                                IdeSourceView *source_view)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_SOURCE_VIEW (source_view));

  ide_page_mark_used (IDE_PAGE (self));

  return GDK_EVENT_PROPAGATE;
}

static void
ide_editor_page_buffer_loaded (IdeEditorPage *self,
                               IdeBuffer     *buffer)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_BUFFER (buffer));

  ide_editor_page_update_icon (self);

  /* Scroll to the insertion location once the buffer
   * has loaded. This is useful if it is not onscreen.
   */
  ide_source_view_scroll_to_insert (self->source_view);
}

static void
ide_editor_page_buffer_modified_changed (IdeEditorPage *self,
                                         IdeBuffer     *buffer)
{
  gboolean modified = FALSE;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_BUFFER (buffer));

  if (!ide_buffer_get_loading (buffer))
    modified = gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (buffer));

  ide_page_set_modified (IDE_PAGE (self), modified);
}

static void
ide_editor_page_buffer_notify_language_cb (IdeExtensionSetAdapter *set,
                                           PeasPluginInfo         *plugin_info,
                                           PeasExtension          *exten,
                                           gpointer                user_data)
{
  const gchar *language_id = user_data;

  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (exten));

  ide_editor_page_addin_language_changed (IDE_EDITOR_PAGE_ADDIN (exten), language_id);
}

static void
ide_editor_page_buffer_notify_language (IdeEditorPage *self,
                                        GParamSpec    *pspec,
                                        IdeBuffer     *buffer)
{
  const gchar *lang_id = NULL;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_BUFFER (buffer));

  if (self->addins == NULL)
    return;

  lang_id = ide_buffer_get_language_id (buffer);

  /* Update extensions that change based on language */
  ide_extension_set_adapter_set_value (self->addins, lang_id);
  ide_extension_set_adapter_foreach (self->addins,
                                     ide_editor_page_buffer_notify_language_cb,
                                     (gpointer)lang_id);

  ide_editor_page_update_icon (self);
}

static void
ide_editor_page_buffer_notify_style_scheme (IdeEditorPage *self,
                                            GParamSpec    *pspec,
                                            IdeBuffer     *buffer)
{
  g_autofree gchar *background = NULL;
  g_autofree gchar *foreground = NULL;
  GtkSourceStyleScheme *scheme;
  GtkSourceStyle *style;
  gboolean background_set = FALSE;
  gboolean foreground_set = FALSE;
  GdkRGBA rgba;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_BUFFER (buffer));

  if (NULL == (scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer))) ||
      NULL == (style = gtk_source_style_scheme_get_style (scheme, "text")))
    goto unset_primary_color;

  g_object_get (style,
                "background-set", &background_set,
                "background", &background,
                "foreground-set", &foreground_set,
                "foreground", &foreground,
                NULL);

  if (!background_set || background == NULL || !gdk_rgba_parse (&rgba, background))
    goto unset_primary_color;

  if (background_set && background != NULL && gdk_rgba_parse (&rgba, background))
    ide_page_set_primary_color_bg (IDE_PAGE (self), &rgba);
  else
    goto unset_primary_color;

  if (foreground_set && foreground != NULL && gdk_rgba_parse (&rgba, foreground))
    ide_page_set_primary_color_fg (IDE_PAGE (self), &rgba);
  else
    ide_page_set_primary_color_fg (IDE_PAGE (self), NULL);

  return;

unset_primary_color:
  ide_page_set_primary_color_bg (IDE_PAGE (self), NULL);
  ide_page_set_primary_color_fg (IDE_PAGE (self), NULL);
}

static void
ide_editor_page__buffer_notify_changed_on_volume (IdeEditorPage *self,
                                                  GParamSpec    *pspec,
                                                  IdeBuffer     *buffer)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_BUFFER (buffer));

  gtk_revealer_set_reveal_child (self->modified_revealer,
                                 ide_buffer_get_changed_on_volume (buffer));
}

static void
ide_editor_page_hide_reload_bar (IdeEditorPage *self,
                                 GtkWidget     *button)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));

  gtk_revealer_set_reveal_child (self->modified_revealer, FALSE);
}

static gboolean
ide_editor_page_source_view_event (IdeEditorPage *self,
                                   GdkEvent      *event,
                                   IdeSourceView *source_view)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (event != NULL);
  g_assert (IDE_IS_SOURCE_VIEW (source_view) || GTK_SOURCE_IS_MAP (source_view));

  if (self->auto_hide_map)
    {
      ide_editor_page_update_reveal_timer (self);
      gtk_revealer_set_reveal_child (self->map_revealer, TRUE);
    }

  return GDK_EVENT_PROPAGATE;
}

static void
ide_editor_page_bind_signals (IdeEditorPage  *self,
                              IdeBuffer      *buffer,
                              DzlSignalGroup *buffer_signals)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_BUFFER (buffer));
  g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals));

  ide_editor_page_buffer_modified_changed (self, buffer);
  ide_editor_page_buffer_notify_language (self, NULL, buffer);
  ide_editor_page_buffer_notify_style_scheme (self, NULL, buffer);
  ide_editor_page_buffer_notify_failed (self, NULL, buffer);
}

static void
ide_editor_page_set_buffer (IdeEditorPage *self,
                            IdeBuffer     *buffer)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (!buffer || IDE_IS_BUFFER (buffer));

  if (g_set_object (&self->buffer, buffer))
    {
      dzl_signal_group_set_target (self->buffer_signals, buffer);
      dzl_binding_group_set_source (self->buffer_bindings, buffer);
      gtk_text_view_set_buffer (GTK_TEXT_VIEW (self->source_view),
                                GTK_TEXT_BUFFER (buffer));
      gtk_drag_dest_unset (GTK_WIDGET (self->source_view));
      ide_editor_page_update_icon (self);
    }
}

static IdePage *
ide_editor_page_create_split (IdePage *view)
{
  IdeEditorPage *self = (IdeEditorPage *)view;

  g_assert (IDE_IS_EDITOR_PAGE (self));

  return g_object_new (IDE_TYPE_EDITOR_PAGE,
                       "buffer", self->buffer,
                       "visible", TRUE,
                       NULL);
}

static void
ide_editor_page_notify_frame_set (IdeExtensionSetAdapter *set,
                                  PeasPluginInfo         *plugin_info,
                                  PeasExtension          *exten,
                                  gpointer                user_data)
{
  IdeFrame *frame = user_data;
  IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten;

  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
  g_assert (IDE_IS_FRAME (frame));

  ide_editor_page_addin_frame_set (addin, frame);
}

static void
ide_editor_page_addin_added (IdeExtensionSetAdapter *set,
                             PeasPluginInfo         *plugin_info,
                             PeasExtension          *exten,
                             gpointer                user_data)
{
  IdeEditorPage *self = user_data;
  IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten;

  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
  g_assert (IDE_IS_EDITOR_PAGE (self));

  ide_editor_page_addin_load (addin, self);

  /*
   * Notify of the current frame, but refetch the frame pointer just
   * to be sure we aren't re-using an old pointer in case we're racing
   * with a finalizer.
   */
  if (self->last_frame_ptr != NULL)
    {
      GtkWidget *frame = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
      if (frame != NULL)
        ide_editor_page_addin_frame_set (addin, IDE_FRAME (frame));
    }
}

static void
ide_editor_page_addin_removed (IdeExtensionSetAdapter *set,
                               PeasPluginInfo         *plugin_info,
                               PeasExtension          *exten,
                               gpointer                user_data)
{
  IdeEditorPage *self = user_data;
  IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten;

  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
  g_assert (IDE_IS_EDITOR_PAGE (self));

  ide_editor_page_addin_unload (addin, self);
}

static void
ide_editor_page_hierarchy_changed (GtkWidget *widget,
                                   GtkWidget *old_toplevel)
{
  IdeEditorPage *self = (IdeEditorPage *)widget;
  IdeFrame *frame;
  IdeContext *context;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));

  /*
   * We don't need to chain up today, but if IdePage starts
   * using the hierarchy_changed signal to handle anything, we want
   * to make sure we aren't surprised.
   */
  if (GTK_WIDGET_CLASS (ide_editor_page_parent_class)->hierarchy_changed)
    GTK_WIDGET_CLASS (ide_editor_page_parent_class)->hierarchy_changed (widget, old_toplevel);

  context = ide_widget_get_context (GTK_WIDGET (self));
  frame = (IdeFrame *)gtk_widget_get_ancestor (widget, IDE_TYPE_FRAME);

  /*
   * We don't want to create addins until the widget has been placed into
   * the widget tree. That way the addins can get access to the context
   * or other useful details.
   */
  if (context != NULL && self->addins == NULL)
    {
      self->addins = ide_extension_set_adapter_new (IDE_OBJECT (context),
                                                    peas_engine_get_default (),
                                                    IDE_TYPE_EDITOR_PAGE_ADDIN,
                                                    "Editor-Page-Languages",
                                                    ide_editor_page_get_language_id (self));

      g_signal_connect (self->addins,
                        "extension-added",
                        G_CALLBACK (ide_editor_page_addin_added),
                        self);

      g_signal_connect (self->addins,
                        "extension-removed",
                        G_CALLBACK (ide_editor_page_addin_removed),
                        self);

      ide_extension_set_adapter_foreach (self->addins,
                                         ide_editor_page_addin_added,
                                         self);
    }

  /*
   * If we have been moved into a new frame, notify the addins of the
   * hierarchy change.
   */
  if (frame != NULL && frame != self->last_frame_ptr && self->addins != NULL)
    {
      self->last_frame_ptr = frame;
      ide_extension_set_adapter_foreach (self->addins,
                                         ide_editor_page_notify_frame_set,
                                         frame);
    }
}

static void
ide_editor_page_update_map (IdeEditorPage *self)
{
  GtkWidget *parent;

  g_assert (IDE_IS_EDITOR_PAGE (self));

  parent = gtk_widget_get_parent (GTK_WIDGET (self->map));

  g_object_ref (self->map);

  gtk_container_remove (GTK_CONTAINER (parent), GTK_WIDGET (self->map));

  if (self->auto_hide_map)
    gtk_container_add (GTK_CONTAINER (self->map_revealer), GTK_WIDGET (self->map));
  else
    gtk_container_add (GTK_CONTAINER (self->scroller_box), GTK_WIDGET (self->map));

  gtk_widget_set_visible (GTK_WIDGET (self->map_revealer), self->show_map && self->auto_hide_map);
  gtk_widget_set_visible (GTK_WIDGET (self->map), self->show_map);
  gtk_revealer_set_reveal_child (self->map_revealer, self->show_map);

  ide_editor_page_update_reveal_timer (self);

  g_object_unref (self->map);
}

static void
ide_editor_page_buffer_notify_file (IdeEditorPage *self,
                                    GParamSpec    *pspec,
                                    gpointer       user_data)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));


}

static void
search_revealer_notify_reveal_child (IdeEditorPage *self,
                                     GParamSpec    *pspec,
                                     GtkRevealer   *revealer)
{
  IdeCompletion *completion;

  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
  g_return_if_fail (pspec != NULL);
  g_return_if_fail (GTK_IS_REVEALER (revealer));

  completion = ide_source_view_get_completion (IDE_SOURCE_VIEW (self->source_view));

  if (!gtk_revealer_get_reveal_child (revealer))
    {
      ide_editor_search_end_interactive (self->search);

      /* Restore completion that we blocked below. */
      ide_completion_unblock_interactive (completion);
    }
  else
    {
      ide_editor_search_begin_interactive (self->search);

      /*
       * Block the completion while the search bar is set. It only
       * slows things down like replace functionality. We'll
       * restore it above when we clear state.
       */
      ide_completion_block_interactive (completion);
    }
}

static void
ide_editor_page_focus_location (IdeEditorPage *self,
                                IdeLocation   *location,
                                IdeSourceView *source_view)
{
  GtkWidget *editor;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (location != NULL);
  g_assert (IDE_IS_SOURCE_VIEW (source_view));

  editor = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_EDITOR_SURFACE);
  ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (editor), location);
}

static void
ide_editor_page_clear_search (IdeEditorPage *self,
                              IdeSourceView *view)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_EDITOR_SEARCH (self->search));
  g_assert (IDE_IS_SOURCE_VIEW (view));

  ide_editor_search_set_search_text (self->search, NULL);
  ide_editor_search_set_visible (self->search, FALSE);
  gtk_revealer_set_reveal_child (self->search_revealer, FALSE);
}

static void
ide_editor_page_move_search (IdeEditorPage    *self,
                             GtkDirectionType  dir,
                             gboolean          extend_selection,
                             gboolean          select_match,
                             gboolean          exclusive,
                             gboolean          apply_count,
                             gboolean          at_word_boundaries,
                             IdeSourceView    *view)
{
  IdeEditorSearchSelect sel = 0;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_EDITOR_SEARCH (self->search));
  g_assert (IDE_IS_SOURCE_VIEW (view));

  if (extend_selection && select_match)
    sel = IDE_EDITOR_SEARCH_SELECT_WITH_RESULT;
  else if (extend_selection)
    sel = IDE_EDITOR_SEARCH_SELECT_TO_RESULT;

  ide_editor_search_set_extend_selection (self->search, sel);
  ide_editor_search_set_visible (self->search, TRUE);

  if (apply_count)
    {
      ide_editor_search_set_repeat (self->search, ide_source_view_get_count (view));
      g_signal_emit_by_name (view, "clear-count");
    }

  ide_editor_search_set_at_word_boundaries (self->search, at_word_boundaries);

  switch (dir)
    {
    case GTK_DIR_DOWN:
    case GTK_DIR_RIGHT:
      ide_editor_search_set_reverse (self->search, FALSE);
      ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
      break;

    case GTK_DIR_TAB_FORWARD:
      if (extend_selection)
        ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_FORWARD);
      else
        ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
      break;

    case GTK_DIR_UP:
    case GTK_DIR_LEFT:
      ide_editor_search_set_reverse (self->search, TRUE);
      ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
      break;

    case GTK_DIR_TAB_BACKWARD:
      if (extend_selection)
        ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_BACKWARD);
      else
        ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_PREVIOUS);
      break;

    default:
      break;
    }
}

static void
ide_editor_page_set_search_text (IdeEditorPage *self,
                                 const gchar   *search_text,
                                 gboolean       from_selection,
                                 IdeSourceView *view)
{
  g_autofree gchar *freeme = NULL;
  GtkTextIter begin;
  GtkTextIter end;

  g_assert (IDE_IS_EDITOR_PAGE (self));
  g_assert (IDE_IS_EDITOR_SEARCH (self->search));
  g_assert (search_text != NULL || from_selection);
  g_assert (IDE_IS_SOURCE_VIEW (view));

  /* Use interactive mode if we're copying from the clipboard, because that
   * is usually going to be followed by focusing the search box and we want
   * to make sure the occurrance count is updated.
   */

  if (from_selection)
    ide_editor_search_begin_interactive (self->search);

  if (from_selection)
    {
      if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end))
        search_text = freeme = gtk_text_iter_get_slice (&begin, &end);
    }

  ide_editor_search_set_search_text (self->search, search_text);
  ide_editor_search_set_regex_enabled (self->search, FALSE);

  if (from_selection)
    ide_editor_search_end_interactive (self->search);
}

static void
ide_editor_page_constructed (GObject *object)
{
  IdeEditorPage *self = (IdeEditorPage *)object;
  GtkSourceGutterRenderer *renderer;
  GtkSourceGutter *gutter;

  g_assert (IDE_IS_EDITOR_PAGE (self));

  G_OBJECT_CLASS (ide_editor_page_parent_class)->constructed (object);

  gutter = gtk_source_view_get_gutter (GTK_SOURCE_VIEW (self->map), GTK_TEXT_WINDOW_LEFT);
  renderer = g_object_new (IDE_TYPE_LINE_CHANGE_GUTTER_RENDERER,
                           "size", 1,
                           "visible", TRUE,
                           NULL);
  gtk_source_gutter_insert (gutter, renderer, 0);

  _ide_editor_page_init_actions (self);
  _ide_editor_page_init_shortcuts (self);
  _ide_editor_page_init_settings (self);

  g_signal_connect_swapped (self->source_view,
                            "focus-in-event",
                            G_CALLBACK (ide_editor_page_focus_in_event),
                            self);

  g_signal_connect_swapped (self->source_view,
                            "motion-notify-event",
                            G_CALLBACK (ide_editor_page_source_view_event),
                            self);

  g_signal_connect_swapped (self->source_view,
                            "scroll-event",
                            G_CALLBACK (ide_editor_page_source_view_event),
                            self);

  g_signal_connect_swapped (self->source_view,
                            "focus-location",
                            G_CALLBACK (ide_editor_page_focus_location),
                            self);

  g_signal_connect_swapped (self->source_view,
                            "set-search-text",
                            G_CALLBACK (ide_editor_page_set_search_text),
                            self);

  g_signal_connect_swapped (self->source_view,
                            "clear-search",
                            G_CALLBACK (ide_editor_page_clear_search),
                            self);

  g_signal_connect_swapped (self->source_view,
                            "move-search",
                            G_CALLBACK (ide_editor_page_move_search),
                            self);

  g_signal_connect_swapped (self->map,
                            "motion-notify-event",
                            G_CALLBACK (ide_editor_page_source_view_event),
                            self);



  /*
   * We want to track when the search revealer is visible. We will discard
   * the search context when the revealer is not visible so that we don't
   * continue performing expensive buffer operations.
   */
  g_signal_connect_swapped (self->search_revealer,
                            "notify::reveal-child",
                            G_CALLBACK (search_revealer_notify_reveal_child),
                            self);

  self->search = ide_editor_search_new (GTK_SOURCE_VIEW (self->source_view));
  ide_editor_search_bar_set_search (self->search_bar, self->search);
  gtk_widget_insert_action_group (GTK_WIDGET (self), "editor-search",
                                  G_ACTION_GROUP (self->search));

  ide_editor_page_load_fonts (self);
  ide_editor_page_update_map (self);
}

static void
ide_editor_page_destroy (GtkWidget *widget)
{
  IdeEditorPage *self = (IdeEditorPage *)widget;

  g_assert (IDE_IS_EDITOR_PAGE (self));

  /*
   * WORKAROUND: We need to reset the drag dest to avoid warnings by Gtk
   * reseting the target list for the source view.
   */
  if (self->source_view != NULL)
    gtk_drag_dest_set (GTK_WIDGET (self->source_view),
                       GTK_DEST_DEFAULT_ALL,
                       NULL, 0, GDK_ACTION_COPY);

  dzl_clear_source (&self->toggle_map_source);

  ide_clear_and_destroy_object (&self->addins);

  gtk_widget_insert_action_group (widget, "editor-search", NULL);
  gtk_widget_insert_action_group (widget, "editor-page", NULL);

  g_cancellable_cancel (self->destroy_cancellable);
  g_clear_object (&self->destroy_cancellable);

  g_clear_object (&self->search);
  g_clear_object (&self->editor_settings);
  g_clear_object (&self->insight_settings);

  g_clear_object (&self->buffer);

  if (self->buffer_bindings != NULL)
    {
      dzl_binding_group_set_source (self->buffer_bindings, NULL);
      g_clear_object (&self->buffer_bindings);
    }

  if (self->buffer_signals != NULL)
    {
      dzl_signal_group_set_target (self->buffer_signals, NULL);
      g_clear_object (&self->buffer_signals);
    }

  GTK_WIDGET_CLASS (ide_editor_page_parent_class)->destroy (widget);
}

static GFile *
ide_editor_page_get_file_or_directory (IdePage *page)
{
  GFile *ret = ide_editor_page_get_file (IDE_EDITOR_PAGE (page));
  return ret ? g_object_ref (ret) : NULL;
}

static void
ide_editor_page_finalize (GObject *object)
{
  G_OBJECT_CLASS (ide_editor_page_parent_class)->finalize (object);

  DZL_COUNTER_DEC (instances);
}

static void
ide_editor_page_get_property (GObject    *object,
                              guint       prop_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  IdeEditorPage *self = IDE_EDITOR_PAGE (object);

  switch (prop_id)
    {
    case PROP_AUTO_HIDE_MAP:
      g_value_set_boolean (value, ide_editor_page_get_auto_hide_map (self));
      break;

    case PROP_BUFFER:
      g_value_set_object (value, ide_editor_page_get_buffer (self));
      break;

    case PROP_BUFFER_FILE:
      g_value_set_object (value, ide_buffer_get_file (self->buffer));
      break;
    case PROP_VIEW:
      g_value_set_object (value, ide_editor_page_get_view (self));
      break;

    case PROP_SEARCH:
      g_value_set_object (value, ide_editor_page_get_search (self));
      break;

    case PROP_SHOW_MAP:
      g_value_set_boolean (value, ide_editor_page_get_show_map (self));
      break;

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

static void
ide_editor_page_set_property (GObject      *object,
                              guint         prop_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  IdeEditorPage *self = IDE_EDITOR_PAGE (object);

  switch (prop_id)
    {
    case PROP_AUTO_HIDE_MAP:
      ide_editor_page_set_auto_hide_map (self, g_value_get_boolean (value));
      break;

    case PROP_BUFFER:
      ide_editor_page_set_buffer (self, g_value_get_object (value));
      break;

    case PROP_SHOW_MAP:
      ide_editor_page_set_show_map (self, g_value_get_boolean (value));
      break;

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

static void
ide_editor_page_class_init (IdeEditorPageClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  IdePageClass *page_class = IDE_PAGE_CLASS (klass);

  object_class->finalize = ide_editor_page_finalize;
  object_class->constructed = ide_editor_page_constructed;
  object_class->get_property = ide_editor_page_get_property;
  object_class->set_property = ide_editor_page_set_property;

  widget_class->destroy = ide_editor_page_destroy;
  widget_class->hierarchy_changed = ide_editor_page_hierarchy_changed;

  page_class->create_split = ide_editor_page_create_split;
  page_class->get_file_or_directory = ide_editor_page_get_file_or_directory;

  properties [PROP_BUFFER] =
    g_param_spec_object ("buffer",
                         "Buffer",
                         "The buffer for the view",
                         IDE_TYPE_BUFFER,
                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  /* It's really just there to get notify:: support for the buffer's file property
   * but through the page, for the session addin.
   */
  properties [PROP_BUFFER_FILE] =
    g_param_spec_object ("buffer-file",
                         "Buffer file",
                         "The buffer file for the view's buffer",
                         G_TYPE_FILE,
                         (G_PARAM_STATIC_STRINGS | G_PARAM_READABLE));

  properties [PROP_SEARCH] =
    g_param_spec_object ("search",
                         "Search",
                         "An search helper for the document",
                         IDE_TYPE_EDITOR_SEARCH,
                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  properties [PROP_SHOW_MAP] =
    g_param_spec_boolean ("show-map",
                          "Show Map",
                          "If the overview map should be shown",
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_AUTO_HIDE_MAP] =
    g_param_spec_boolean ("auto-hide-map",
                          "Auto Hide Map",
                          "If the overview map should be auto-hidden",
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_VIEW] =
    g_param_spec_object ("view",
                         "View",
                         "The view for editing the buffer",
                         IDE_TYPE_SOURCE_VIEW,
                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-editor/ui/ide-editor-page.ui");
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, map);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, map_revealer);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, overlay);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, progress_bar);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, scroller);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, scroller_box);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, search_bar);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, search_revealer);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, modified_revealer);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, modified_cancel_button);
  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, source_view);
  gtk_widget_class_bind_template_callback (widget_class, ide_editor_page_notify_child_revealed);
  gtk_widget_class_bind_template_callback (widget_class, ide_editor_page_stop_search);

  g_type_ensure (IDE_TYPE_SOURCE_VIEW);
  g_type_ensure (IDE_TYPE_EDITOR_SEARCH_BAR);
}

static void
ide_editor_page_init (IdeEditorPage *self)
{
  DZL_COUNTER_INC (instances);

  gtk_widget_init_template (GTK_WIDGET (self));

  ide_page_set_can_split (IDE_PAGE (self), TRUE);
  ide_page_set_menu_id (IDE_PAGE (self), "ide-editor-page-document-menu");

  self->destroy_cancellable = g_cancellable_new ();

  /* Setup signals to monitor on the buffer. */
  self->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER);

  dzl_signal_group_connect_swapped (self->buffer_signals,
                                    "loaded",
                                    G_CALLBACK (ide_editor_page_buffer_loaded),
                                    self);

  dzl_signal_group_connect_swapped (self->buffer_signals,
                                    "modified-changed",
                                    G_CALLBACK (ide_editor_page_buffer_modified_changed),
                                    self);

  dzl_signal_group_connect_swapped (self->buffer_signals,
                                    "notify::file",
                                    G_CALLBACK (ide_editor_page_buffer_notify_file),
                                    self);

  dzl_signal_group_connect_swapped (self->buffer_signals,
                                    "notify::failed",
                                    G_CALLBACK (ide_editor_page_buffer_notify_failed),
                                    self);

  dzl_signal_group_connect_swapped (self->buffer_signals,
                                    "notify::language",
                                    G_CALLBACK (ide_editor_page_buffer_notify_language),
                                    self);

  dzl_signal_group_connect_swapped (self->buffer_signals,
                                    "notify::style-scheme",
                                    G_CALLBACK (ide_editor_page_buffer_notify_style_scheme),
                                    self);
  dzl_signal_group_connect_swapped (self->buffer_signals,
                                    "notify::changed-on-volume",
                                    G_CALLBACK (ide_editor_page__buffer_notify_changed_on_volume),
                                    self);

  g_signal_connect_swapped (self->buffer_signals,
                            "bind",
                            G_CALLBACK (ide_editor_page_bind_signals),
                            self);

  g_signal_connect_object (self->modified_cancel_button,
                           "clicked",
                           G_CALLBACK (ide_editor_page_hide_reload_bar),
                           self,
                           G_CONNECT_SWAPPED);

  /* Setup bindings for the buffer. */
  self->buffer_bindings = dzl_binding_group_new ();
  dzl_binding_group_bind (self->buffer_bindings, "title", self, "title", 0);

  /* Load our custom font for the overview map. */
  gtk_source_map_set_view (self->map, GTK_SOURCE_VIEW (self->source_view));
}

/**
 * ide_editor_page_get_buffer:
 * @self: a #IdeEditorPage
 *
 * Gets the underlying buffer for the view.
 *
 * Returns: (transfer none): An #IdeBuffer
 *
 * Since: 3.32
 */
IdeBuffer *
ide_editor_page_get_buffer (IdeEditorPage *self)
{
  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);

  return self->buffer;
}

/**
 * ide_editor_page_get_view:
 * @self: a #IdeEditorPage
 *
 * Gets the #IdeSourceView that is part of the #IdeEditorPage.
 *
 * Returns: (transfer none): An #IdeSourceView
 *
 * Since: 3.32
 */
IdeSourceView *
ide_editor_page_get_view (IdeEditorPage *self)
{
  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);

  return self->source_view;
}

/**
 * ide_editor_page_get_language_id:
 * @self: a #IdeEditorPage
 *
 * This is a helper to get the language-id of the underlying buffer.
 *
 * Returns: (nullable): the language-id as a string, or %NULL
 *
 * Since: 3.32
 */
const gchar *
ide_editor_page_get_language_id (IdeEditorPage *self)
{
  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);

  if (self->buffer != NULL)
    {
      GtkSourceLanguage *language;

      language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self->buffer));

      if (language != NULL)
        return gtk_source_language_get_id (language);
    }

  return NULL;
}

/**
 * ide_editor_page_scroll_to_line:
 * @self: a #IdeEditorPage
 * @line: the line to scroll to
 *
 * This is a helper to quickly jump to a given line without all the frills. It
 * will also ensure focus on the editor view, so that refocusing the view
 * afterwards does not cause the view to restore the cursor to the previous
 * location.
 *
 * This will move the insert cursor.
 *
 * Lines start from 0.
 *
 * Since: 3.32
 */
void
ide_editor_page_scroll_to_line (IdeEditorPage *self,
                                guint          line)
{
  ide_editor_page_scroll_to_line_offset (self, line, 0);
}

/**
 * ide_editor_page_scroll_to_line_offset:
 * @self: a #IdeEditorPage
 * @line: the line to scroll to
 * @line_offset: the line offset
 *
 * Like ide_editor_page_scroll_to_line() but allows specifying the
 * line offset (column) to place the cursor on.
 *
 * This will move the insert cursor.
 *
 * Lines and offsets start from 0.
 *
 * If @line_offset is zero, the first non-space character of @line will be
 * used instead.
 *
 * Since: 3.32
 */
void
ide_editor_page_scroll_to_line_offset (IdeEditorPage *self,
                                       guint          line,
                                       guint          line_offset)
{
  GtkTextIter iter;

  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
  g_return_if_fail (self->buffer != NULL);
  g_return_if_fail (line <= G_MAXINT);

  gtk_widget_grab_focus (GTK_WIDGET (self->source_view));

  gtk_text_buffer_get_iter_at_line_offset (GTK_TEXT_BUFFER (self->buffer), &iter,
                                           line, line_offset);

  if (line_offset == 0)
    {
      while (!gtk_text_iter_ends_line (&iter) &&
             g_unichar_isspace (gtk_text_iter_get_char (&iter)))
        {
          if (!gtk_text_iter_forward_char (&iter))
            break;
        }
    }

  gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self->buffer), &iter, &iter);
  ide_source_view_scroll_to_insert (self->source_view);
}

gboolean
ide_editor_page_get_auto_hide_map (IdeEditorPage *self)
{
  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), FALSE);

  return self->auto_hide_map;
}

static gboolean
ide_editor_page_auto_hide_cb (gpointer user_data)
{
  IdeEditorPage *self = user_data;

  g_assert (IDE_IS_EDITOR_PAGE (self));

  self->toggle_map_source = 0;
  gtk_revealer_set_reveal_child (self->map_revealer, FALSE);

  return G_SOURCE_REMOVE;
}

static void
ide_editor_page_update_reveal_timer (IdeEditorPage *self)
{
  g_assert (IDE_IS_EDITOR_PAGE (self));

  dzl_clear_source (&self->toggle_map_source);

  if (self->auto_hide_map && gtk_revealer_get_reveal_child (self->map_revealer))
    {
      self->toggle_map_source =
        gdk_threads_add_timeout_seconds_full (G_PRIORITY_LOW,
                                              AUTO_HIDE_TIMEOUT_SECONDS,
                                              ide_editor_page_auto_hide_cb,
                                              g_object_ref (self),
                                              g_object_unref);
    }
}

void
ide_editor_page_set_auto_hide_map (IdeEditorPage *self,
                                   gboolean       auto_hide_map)
{
  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));

  auto_hide_map = !!auto_hide_map;

  if (auto_hide_map != self->auto_hide_map)
    {
      self->auto_hide_map = auto_hide_map;
      ide_editor_page_update_map (self);
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_AUTO_HIDE_MAP]);
    }
}

gboolean
ide_editor_page_get_show_map (IdeEditorPage *self)
{
  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), FALSE);

  return self->show_map;
}

void
ide_editor_page_set_show_map (IdeEditorPage *self,
                              gboolean       show_map)
{
  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));

  show_map = !!show_map;

  if (show_map != self->show_map)
    {
      self->show_map = show_map;
      g_object_set (self->scroller,
                    "vscrollbar-policy", show_map ? GTK_POLICY_EXTERNAL : GTK_POLICY_AUTOMATIC,
                    NULL);
      ide_editor_page_update_map (self);
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_AUTO_HIDE_MAP]);
    }
}

/**
 * ide_editor_page_set_language:
 * @self: a #IdeEditorPage
 *
 * This is a convenience function to set the language on the underlying
 * #IdeBuffer text buffer.
 *
 * Since: 3.32
 */
void
ide_editor_page_set_language (IdeEditorPage     *self,
                              GtkSourceLanguage *language)
{
  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
  g_return_if_fail (!language || GTK_SOURCE_IS_LANGUAGE (language));

  gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (self->buffer), language);
}

/**
 * ide_editor_page_get_language:
 * @self: a #IdeEditorPage
 *
 * Gets the #GtkSourceLanguage that is used by the underlying buffer.
 *
 * Returns: (transfer none) (nullable): a #GtkSourceLanguage or %NULL.
 *
 * Since: 3.32
 */
GtkSourceLanguage *
ide_editor_page_get_language (IdeEditorPage *self)
{
  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);

  return gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self->buffer));
}

/**
 * ide_editor_page_move_next_error:
 * @self: a #IdeEditorPage
 *
 * Moves to the next error, if any.
 *
 * If there is no error, the insertion cursor is not moved.
 *
 * Since: 3.32
 */
void
ide_editor_page_move_next_error (IdeEditorPage *self)
{
  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));

  g_signal_emit_by_name (self->source_view, "move-error", GTK_DIR_DOWN);
}

/**
 * ide_editor_page_move_previous_error:
 * @self: a #IdeEditorPage
 *
 * Moves the insertion cursor to the previous error.
 *
 * If there is no error, the insertion cursor is not moved.
 *
 * Since: 3.32
 */
void
ide_editor_page_move_previous_error (IdeEditorPage *self)
{
  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));

  g_signal_emit_by_name (self->source_view, "move-error", GTK_DIR_UP);
}

/**
 * ide_editor_page_move_next_search_result:
 * @self: a #IdeEditorPage
 *
 * Moves the insertion cursor to the next search result.
 *
 * If there is no search result, the insertion cursor is not moved.
 *
 * Since: 3.32
 */
void
ide_editor_page_move_next_search_result (IdeEditorPage *self)
{
  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
  g_return_if_fail (self->destroy_cancellable != NULL);
  g_return_if_fail (self->buffer != NULL);

  ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
}

/**
 * ide_editor_page_move_previous_search_result:
 * @self: a #IdeEditorPage
 *
 * Moves the insertion cursor to the previous search result.
 *
 * If there is no search result, the insertion cursor is not moved.
 *
 * Since: 3.32
 */
void
ide_editor_page_move_previous_search_result (IdeEditorPage *self)
{
  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
  g_return_if_fail (self->destroy_cancellable != NULL);
  g_return_if_fail (self->buffer != NULL);

  ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_PREVIOUS);
}

/**
 * ide_editor_page_get_search:
 * @self: a #IdeEditorPage
 *
 * Gets the #IdeEditorSearch used to search within the document.
 *
 * Returns: (transfer none): An #IdeEditorSearch
 *
 * Since: 3.32
 */
IdeEditorSearch *
ide_editor_page_get_search (IdeEditorPage *self)
{
  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);

  return self->search;
}

/**
 * ide_editor_page_get_file:
 * @self: a #IdeEditorPage
 *
 * Gets the #GFile that represents the current file. This may be a temporary
 * file, but a #GFile will still be used for the temporary file.
 *
 * Returns: (transfer none): a #GFile for the current buffer
 *
 * Since: 3.32
 */
GFile *
ide_editor_page_get_file (IdeEditorPage *self)
{
  IdeBuffer *buffer;

  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);

  if ((buffer = ide_editor_page_get_buffer (self)))
    return ide_buffer_get_file (buffer);

  return NULL;
}
