[gnome-builder] history: add a history plugin for navigation within the editor



commit 2e6dc3e632e799feae410efc3a879a15a1f981d9
Author: Christian Hergert <chergert redhat com>
Date:   Mon Sep 4 15:20:11 2017 -0700

    history: add a history plugin for navigation within the editor
    
    This can be used for rudimentary history browsing within the editor based
    on jump/edit locations.
    
    Ctrl+O/Ctrl+I in Vim mode can activate the history in a somewhat similar
    fashion to Vim (although not perfect).
    
    This could still use some more work to ensure we stay within the proper
    layout stack.

 data/keybindings/vim.css                         |    4 +-
 meson_options.txt                                |    1 +
 plugins/history/gbp-history-editor-view-addin.c  |  291 ++++++++++++++
 plugins/history/gbp-history-editor-view-addin.h  |   29 ++
 plugins/history/gbp-history-item.c               |  221 +++++++++++
 plugins/history/gbp-history-item.h               |   37 ++
 plugins/history/gbp-history-layout-stack-addin.c |  443 ++++++++++++++++++++++
 plugins/history/gbp-history-layout-stack-addin.h |   34 ++
 plugins/history/gbp-history-plugin.c             |   34 ++
 plugins/history/history.plugin                   |    8 +
 plugins/history/meson.build                      |   30 ++
 plugins/meson.build                              |    2 +
 12 files changed, 1132 insertions(+), 2 deletions(-)
---
diff --git a/data/keybindings/vim.css b/data/keybindings/vim.css
index 55ec239..773c292 100644
--- a/data/keybindings/vim.css
+++ b/data/keybindings/vim.css
@@ -548,8 +548,8 @@
   bind "<ctrl>v" { "set-mode" ("vim-visual-block", permanent) };
 
   /* navigation */
-  bind "<ctrl>o" { "action" ("layoutstack", "go-backward", "") };
-  bind "<ctrl>i" { "action" ("layoutstack", "go-forward", "") };
+  bind "<ctrl>o" { "action" ("history", "move-previous-edit", "") };
+  bind "<ctrl>i" { "action" ("history", "move-next-edit", "") };
 
   /* window controls */
   bind "<ctrl>w" { "set-mode" ("vim-normal-ctrl-w", transient) };
diff --git a/meson_options.txt b/meson_options.txt
index 406efbd..6cf20b6 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -46,6 +46,7 @@ option('with_gdb', type: 'boolean')
 option('with_gettext', type: 'boolean')
 option('with_git', type: 'boolean')
 option('with_gnome_code_assistance', type: 'boolean')
+option('with_history', type: 'boolean')
 option('with_html_completion', type: 'boolean')
 option('with_html_preview', type: 'boolean')
 option('with_jedi', type: 'boolean')
diff --git a/plugins/history/gbp-history-editor-view-addin.c b/plugins/history/gbp-history-editor-view-addin.c
new file mode 100644
index 0000000..f2939d2
--- /dev/null
+++ b/plugins/history/gbp-history-editor-view-addin.c
@@ -0,0 +1,291 @@
+/* gbp-history-editor-view-addin.c
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-history-editor-view-addin"
+
+#include "gbp-history-editor-view-addin.h"
+#include "gbp-history-item.h"
+#include "gbp-history-layout-stack-addin.h"
+
+struct _GbpHistoryEditorViewAddin
+{
+  GObject                     parent_instance;
+
+  /* Unowned pointer */
+  IdeEditorView              *editor;
+
+  /* Weak pointer */
+  GbpHistoryLayoutStackAddin *stack_addin;
+
+  gsize                       last_change_count;
+
+  guint                       queued_edit_line;
+  guint                       queued_edit_source;
+};
+
+static void
+gbp_history_editor_view_addin_stack_set (IdeEditorViewAddin *addin,
+                                         IdeLayoutStack     *stack)
+{
+  GbpHistoryEditorViewAddin *self = (GbpHistoryEditorViewAddin *)addin;
+  IdeLayoutStackAddin *stack_addin;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EDITOR_VIEW_ADDIN (self));
+  g_assert (IDE_IS_LAYOUT_STACK (stack));
+
+  stack_addin = ide_layout_stack_addin_find_by_module_name (stack, "history-plugin");
+
+  g_assert (stack_addin != NULL);
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (stack_addin));
+
+  ide_set_weak_pointer (&self->stack_addin, GBP_HISTORY_LAYOUT_STACK_ADDIN (stack_addin));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_history_editor_view_addin_push (GbpHistoryEditorViewAddin *self,
+                                    const GtkTextIter         *iter)
+{
+  g_autoptr(GbpHistoryItem) item = NULL;
+  GtkTextBuffer *buffer;
+  GtkTextMark *mark;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_VIEW_ADDIN (self));
+  g_assert (iter != NULL);
+  g_assert (self->editor != NULL);
+
+  if (self->stack_addin == NULL)
+    IDE_GOTO (no_stack_loaded);
+
+  /*
+   * Create an unnamed mark for this history item, and push the history
+   * item into the stacks history.
+   */
+  buffer = gtk_text_iter_get_buffer (iter);
+  mark = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+  item = gbp_history_item_new (mark);
+
+  gbp_history_layout_stack_addin_push (self->stack_addin, item);
+
+no_stack_loaded:
+  IDE_EXIT;
+}
+
+static void
+gbp_history_editor_view_addin_jump (GbpHistoryEditorViewAddin *self,
+                                    const GtkTextIter         *from,
+                                    const GtkTextIter         *to,
+                                    IdeSourceView             *source_view)
+{
+  IdeBuffer *buffer;
+  gsize change_count;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_VIEW_ADDIN (self));
+  g_assert (from != NULL);
+  g_assert (to != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (source_view));
+
+  buffer = IDE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view)));
+  change_count = ide_buffer_get_change_count (buffer);
+
+  /*
+   * If the buffer has changed since the last jump was recorded,
+   * we want to track this as an edit point so that we can come
+   * back to it later.
+   */
+
+#if 0
+  g_print ("Cursor jumped from %u:%u\n",
+           gtk_text_iter_get_line (iter) + 1,
+           gtk_text_iter_get_line_offset (iter) + 1);
+  g_print ("Now=%lu Prev=%lu\n", change_count, self->last_change_count);
+#endif
+
+  //if (change_count != self->last_change_count)
+    {
+      self->last_change_count = change_count;
+      gbp_history_editor_view_addin_push (self, from);
+      gbp_history_editor_view_addin_push (self, to);
+    }
+}
+
+static gboolean
+gbp_history_editor_view_addin_flush_edit (gpointer user_data)
+{
+  GbpHistoryEditorViewAddin *self = user_data;
+  IdeBuffer *buffer;
+  GtkTextIter iter;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_VIEW_ADDIN (self));
+  g_assert (self->editor != NULL);
+
+  self->queued_edit_source = 0;
+
+  buffer = ide_editor_view_get_buffer (self->editor);
+  gtk_text_buffer_get_iter_at_line (GTK_TEXT_BUFFER (buffer), &iter, self->queued_edit_line);
+  gbp_history_editor_view_addin_push (self, &iter);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_history_editor_view_addin_queue (GbpHistoryEditorViewAddin *self,
+                                     guint                      line)
+{
+  /*
+   * If the buffer is modified, we want to keep track of this position in the
+   * history (the layout stack will automatically merge it with the previous
+   * entry if they are close).
+   *
+   * However, the insert-text signal can happen in rapid succession, so we only
+   * want to deal with it after a small timeout to coallesce the entries into a
+   * single push() into the history stack.
+   */
+
+  if (self->queued_edit_source == 0)
+    {
+      self->queued_edit_line = line;
+      self->queued_edit_source = gdk_threads_add_idle_full (G_PRIORITY_LOW,
+                                                            gbp_history_editor_view_addin_flush_edit,
+                                                            g_object_ref (self),
+                                                            g_object_unref);
+    }
+}
+
+static void
+gbp_history_editor_view_addin_insert_text (GbpHistoryEditorViewAddin *self,
+                                           const GtkTextIter         *location,
+                                           const gchar               *text,
+                                           gint                       length,
+                                           IdeBuffer                 *buffer)
+{
+  g_assert (GBP_IS_HISTORY_EDITOR_VIEW_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (location != NULL);
+  g_assert (text != NULL);
+
+  gbp_history_editor_view_addin_queue (self, gtk_text_iter_get_line (location));
+}
+
+static void
+gbp_history_editor_view_addin_delete_range (GbpHistoryEditorViewAddin *self,
+                                            const GtkTextIter         *begin,
+                                            const GtkTextIter         *end,
+                                            IdeBuffer                 *buffer)
+{
+  g_assert (GBP_IS_HISTORY_EDITOR_VIEW_ADDIN (self));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  gbp_history_editor_view_addin_queue (self, gtk_text_iter_get_line (begin));
+}
+
+static void
+gbp_history_editor_view_addin_load (IdeEditorViewAddin *addin,
+                                    IdeEditorView      *view)
+{
+  GbpHistoryEditorViewAddin *self = (GbpHistoryEditorViewAddin *)addin;
+  IdeSourceView *source_view;
+  IdeBuffer *buffer;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_VIEW_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_VIEW (view));
+
+  self->editor = view;
+
+  buffer = ide_editor_view_get_buffer (view);
+  source_view = ide_editor_view_get_view (view);
+
+  self->last_change_count = ide_buffer_get_change_count (buffer);
+
+  g_signal_connect_swapped (source_view,
+                            "jump",
+                            G_CALLBACK (gbp_history_editor_view_addin_jump),
+                            addin);
+
+  g_signal_connect_swapped (buffer,
+                            "insert-text",
+                            G_CALLBACK (gbp_history_editor_view_addin_insert_text),
+                            self);
+
+  g_signal_connect_swapped (buffer,
+                            "delete-range",
+                            G_CALLBACK (gbp_history_editor_view_addin_delete_range),
+                            self);
+}
+
+static void
+gbp_history_editor_view_addin_unload (IdeEditorViewAddin *addin,
+                                      IdeEditorView      *view)
+{
+  GbpHistoryEditorViewAddin *self = (GbpHistoryEditorViewAddin *)addin;
+  IdeSourceView *source_view;
+  IdeBuffer *buffer;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_VIEW_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_VIEW (view));
+
+  ide_clear_source (&self->queued_edit_source);
+
+  source_view = ide_editor_view_get_view (view);
+  buffer = ide_editor_view_get_buffer (view);
+
+  g_signal_handlers_disconnect_by_func (source_view,
+                                        G_CALLBACK (gbp_history_editor_view_addin_jump),
+                                        self);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (gbp_history_editor_view_addin_insert_text),
+                                        self);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (gbp_history_editor_view_addin_delete_range),
+                                        self);
+
+  ide_clear_weak_pointer (&self->stack_addin);
+
+  self->editor = NULL;
+}
+
+static void
+editor_view_addin_iface_init (IdeEditorViewAddinInterface *iface)
+{
+  iface->load = gbp_history_editor_view_addin_load;
+  iface->unload = gbp_history_editor_view_addin_unload;
+  iface->stack_set = gbp_history_editor_view_addin_stack_set;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpHistoryEditorViewAddin, gbp_history_editor_view_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_VIEW_ADDIN,
+                                                editor_view_addin_iface_init))
+
+static void
+gbp_history_editor_view_addin_class_init (GbpHistoryEditorViewAddinClass *klass)
+{
+}
+
+static void
+gbp_history_editor_view_addin_init (GbpHistoryEditorViewAddin *self)
+{
+}
diff --git a/plugins/history/gbp-history-editor-view-addin.h b/plugins/history/gbp-history-editor-view-addin.h
new file mode 100644
index 0000000..2aee95d
--- /dev/null
+++ b/plugins/history/gbp-history-editor-view-addin.h
@@ -0,0 +1,29 @@
+/* gbp-history-editor-view-addin.h
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#pragma once
+
+#include <ide.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_HISTORY_EDITOR_VIEW_ADDIN (gbp_history_editor_view_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpHistoryEditorViewAddin, gbp_history_editor_view_addin, GBP, 
HISTORY_EDITOR_VIEW_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/plugins/history/gbp-history-item.c b/plugins/history/gbp-history-item.c
new file mode 100644
index 0000000..3e4bc8a
--- /dev/null
+++ b/plugins/history/gbp-history-item.c
@@ -0,0 +1,221 @@
+/* gbp-history-item.c
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-history-item"
+
+#include "gbp-history-item.h"
+
+#define DISTANCE_LINES_THRESH 10
+
+struct _GbpHistoryItem
+{
+  GObject      parent_instance;
+
+  IdeContext  *context;
+  GtkTextMark *mark;
+  GFile       *file;
+
+  guint        line;
+};
+
+G_DEFINE_TYPE (GbpHistoryItem, gbp_history_item, G_TYPE_OBJECT)
+
+static void
+gbp_history_item_dispose (GObject *object)
+{
+  GbpHistoryItem *self = (GbpHistoryItem *)object;
+
+  ide_clear_weak_pointer (&self->context);
+
+  if (self->mark != NULL)
+    {
+      GtkTextBuffer *buffer = gtk_text_mark_get_buffer (self->mark);
+
+      if (buffer != NULL)
+        gtk_text_buffer_delete_mark (buffer, self->mark);
+    }
+
+  g_clear_object (&self->mark);
+  g_clear_object (&self->file);
+
+  G_OBJECT_CLASS (gbp_history_item_parent_class)->dispose (object);
+}
+
+static void
+gbp_history_item_class_init (GbpHistoryItemClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_history_item_dispose;
+}
+
+static void
+gbp_history_item_init (GbpHistoryItem *self)
+{
+}
+
+GbpHistoryItem *
+gbp_history_item_new (GtkTextMark *mark)
+{
+  GtkTextIter iter;
+  GbpHistoryItem *item;
+  GtkTextBuffer *buffer;
+  IdeContext *context;
+  IdeFile *file;
+
+  g_return_val_if_fail (GTK_IS_TEXT_MARK (mark), NULL);
+
+  buffer = gtk_text_mark_get_buffer (mark);
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
+
+  item = g_object_new (GBP_TYPE_HISTORY_ITEM, NULL);
+  item->mark = g_object_ref (mark);
+
+  context = ide_buffer_get_context (IDE_BUFFER (buffer));
+  ide_set_weak_pointer (&item->context, context);
+
+  gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
+  item->line = gtk_text_iter_get_line (&iter);
+
+  file = ide_buffer_get_file (IDE_BUFFER (buffer));
+  item->file = g_object_ref (ide_file_get_file (file));
+
+  return item;
+}
+
+gboolean
+gbp_history_item_chain (GbpHistoryItem *self,
+                        GbpHistoryItem *other)
+{
+  g_return_val_if_fail (GBP_IS_HISTORY_ITEM (self), FALSE);
+  g_return_val_if_fail (GBP_IS_HISTORY_ITEM (other), FALSE);
+
+  if (gtk_text_mark_get_buffer (self->mark) == gtk_text_mark_get_buffer (other->mark))
+    {
+      GtkTextBuffer *buffer = gtk_text_mark_get_buffer (self->mark);
+      GtkTextIter self_iter;
+      GtkTextIter other_iter;
+
+      gtk_text_buffer_get_iter_at_mark (buffer, &self_iter, self->mark);
+      gtk_text_buffer_get_iter_at_mark (buffer, &other_iter, other->mark);
+
+      if (ABS (gtk_text_iter_get_line (&self_iter) -
+               gtk_text_iter_get_line (&other_iter)) < DISTANCE_LINES_THRESH)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+gchar *
+gbp_history_item_get_label (GbpHistoryItem *self)
+{
+  GtkTextBuffer *buffer;
+  const gchar *title;
+  GtkTextIter iter;
+  guint line;
+
+  g_return_val_if_fail (GBP_IS_HISTORY_ITEM (self), NULL);
+  g_return_val_if_fail (self->mark != NULL, NULL);
+
+  buffer = gtk_text_mark_get_buffer (self->mark);
+  if (buffer == NULL)
+    return NULL;
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
+
+  gtk_text_buffer_get_iter_at_mark (buffer, &iter, self->mark);
+  line = gtk_text_iter_get_line (&iter) + 1;
+  title = ide_buffer_get_title (IDE_BUFFER (buffer));
+
+  return g_strdup_printf ("%s <span fgcolor='32767'>%u</span>", title, line);
+}
+
+/**
+ * gbp_history_item_get_location:
+ * @self: a #GbpHistoryItem
+ *
+ * Gets an #IdeSourceLocation represented by this item.
+ *
+ * Returns: (transfer full): A new #IdeSourceLocation
+ */
+IdeSourceLocation *
+gbp_history_item_get_location (GbpHistoryItem *self)
+{
+  GtkTextBuffer *buffer;
+  GtkTextIter iter;
+
+  g_return_val_if_fail (GBP_IS_HISTORY_ITEM (self), NULL);
+  g_return_val_if_fail (self->mark != NULL, NULL);
+
+  if (self->context == NULL)
+    return NULL;
+
+  buffer = gtk_text_mark_get_buffer (self->mark);
+
+  if (buffer == NULL)
+    {
+      g_autoptr(IdeFile) file = ide_file_new (self->context, self->file);
+      return ide_source_location_new (file, self->line, 0, 0);
+    }
+
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
+  gtk_text_buffer_get_iter_at_mark (buffer, &iter, self->mark);
+
+  return ide_buffer_get_iter_location (IDE_BUFFER (buffer), &iter);
+}
+
+/**
+ * gbp_history_item_get_file:
+ *
+ * Returns: (transfer none): A #GFile.
+ */
+GFile *
+gbp_history_item_get_file (GbpHistoryItem *self)
+{
+  g_return_val_if_fail (GBP_IS_HISTORY_ITEM (self), NULL);
+
+  return self->file;
+}
+
+/**
+ * gbp_history_item_get_line:
+ *
+ * Gets the line for the history item.
+ *
+ * If the text mark is still valid, it will be used to locate the
+ * mark which may have moved.
+ */
+guint
+gbp_history_item_get_line (GbpHistoryItem *self)
+{
+  GtkTextBuffer *buffer;
+
+  g_return_val_if_fail (GBP_IS_HISTORY_ITEM (self), 0);
+
+  buffer = gtk_text_mark_get_buffer (self->mark);
+
+  if (buffer != NULL)
+    {
+      GtkTextIter iter;
+
+      gtk_text_buffer_get_iter_at_mark (buffer, &iter, self->mark);
+      return gtk_text_iter_get_line (&iter);
+    }
+
+  return self->line;
+}
diff --git a/plugins/history/gbp-history-item.h b/plugins/history/gbp-history-item.h
new file mode 100644
index 0000000..6e2ae80
--- /dev/null
+++ b/plugins/history/gbp-history-item.h
@@ -0,0 +1,37 @@
+/* gbp-history-item.h
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#pragma once
+
+#include <ide.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_HISTORY_ITEM (gbp_history_item_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpHistoryItem, gbp_history_item, GBP, HISTORY_ITEM, GObject)
+
+GbpHistoryItem    *gbp_history_item_new          (GtkTextMark    *mark);
+gchar             *gbp_history_item_get_label    (GbpHistoryItem *self);
+IdeSourceLocation *gbp_history_item_get_location (GbpHistoryItem *self);
+GFile             *gbp_history_item_get_file     (GbpHistoryItem *self);
+guint              gbp_history_item_get_line     (GbpHistoryItem *self);
+gboolean           gbp_history_item_chain        (GbpHistoryItem *self,
+                                                  GbpHistoryItem *other);
+
+G_END_DECLS
diff --git a/plugins/history/gbp-history-layout-stack-addin.c 
b/plugins/history/gbp-history-layout-stack-addin.c
new file mode 100644
index 0000000..093bd4f
--- /dev/null
+++ b/plugins/history/gbp-history-layout-stack-addin.c
@@ -0,0 +1,443 @@
+/* gbp-history-layout-stack-addin.c
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-history-layout-stack-addin"
+
+#include "gbp-history-layout-stack-addin.h"
+
+#define MAX_HISTORY_ITEMS   20
+#define NEARBY_LINES_THRESH 10
+
+struct _GbpHistoryLayoutStackAddin
+{
+  GObject         parent_instance;
+
+  GListStore     *back_store;
+  GListStore     *forward_store;
+
+  GtkBox         *controls;
+  GtkButton      *previous_button;
+  GtkButton      *next_button;
+
+  IdeLayoutStack *stack;
+
+  guint           navigating;
+};
+
+static void
+gbp_history_layout_stack_addin_update (GbpHistoryLayoutStackAddin *self)
+{
+  gboolean has_items;
+
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (self));
+
+  has_items = g_list_model_get_n_items (G_LIST_MODEL (self->back_store)) > 0;
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->controls),
+                             "history", "move-previous-edit",
+                             "enabled", has_items,
+                             NULL);
+
+  has_items = g_list_model_get_n_items (G_LIST_MODEL (self->forward_store)) > 0;
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->controls),
+                             "history", "move-next-edit",
+                             "enabled", has_items,
+                             NULL);
+
+#if 0
+  g_print ("Backward\n");
+
+  for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->back_store)); i++)
+    {
+      g_autoptr(GbpHistoryItem) item = g_list_model_get_item (G_LIST_MODEL (self->back_store), i);
+
+      g_print ("%s\n", gbp_history_item_get_label (item));
+    }
+
+  g_print ("Forward\n");
+
+  for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->forward_store)); i++)
+    {
+      g_autoptr(GbpHistoryItem) item = g_list_model_get_item (G_LIST_MODEL (self->forward_store), i);
+
+      g_print ("%s\n", gbp_history_item_get_label (item));
+    }
+#endif
+}
+
+static void
+gbp_history_layout_stack_addin_navigate (GbpHistoryLayoutStackAddin *self,
+                                         GbpHistoryItem             *item)
+{
+  g_autoptr(IdeSourceLocation) location = NULL;
+  GtkWidget *editor;
+
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (self));
+  g_assert (GBP_IS_HISTORY_ITEM (item));
+
+  location = gbp_history_item_get_location (item);
+  editor = gtk_widget_get_ancestor (GTK_WIDGET (self->controls), IDE_TYPE_EDITOR_PERSPECTIVE);
+  ide_editor_perspective_focus_location (IDE_EDITOR_PERSPECTIVE (editor), location);
+
+  gbp_history_layout_stack_addin_update (self);
+}
+
+static gboolean
+item_is_nearby (IdeEditorView  *editor,
+                GbpHistoryItem *item)
+{
+  GtkTextIter insert;
+  IdeBuffer *buffer;
+  GFile *buffer_file;
+  GFile *item_file;
+  gint buffer_line;
+  gint item_line;
+
+  g_assert (IDE_IS_EDITOR_VIEW (editor));
+  g_assert (GBP_IS_HISTORY_ITEM (item));
+
+  buffer = ide_editor_view_get_buffer (editor);
+
+  /* Make sure this is the same file */
+  buffer_file = ide_file_get_file (ide_buffer_get_file (buffer));
+  item_file = gbp_history_item_get_file (item);
+  if (!g_file_equal (buffer_file, item_file))
+    return FALSE;
+
+  /* Check if the lines are nearby */
+  ide_buffer_get_selection_bounds (buffer, &insert, NULL);
+  buffer_line = gtk_text_iter_get_line (&insert);
+  item_line = gbp_history_item_get_line (item);
+
+  return ABS (buffer_line - item_line) < NEARBY_LINES_THRESH;
+}
+
+static void
+move_previous_edit_action (GSimpleAction *action,
+                           GVariant      *param,
+                           gpointer       user_data)
+{
+  GbpHistoryLayoutStackAddin *self = user_data;
+  IdeLayoutView *current;
+  GListModel *model;
+  guint n_items;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (self));
+  g_assert (self->stack != NULL);
+
+  model = G_LIST_MODEL (self->back_store);
+  n_items = g_list_model_get_n_items (model);
+  current = ide_layout_stack_get_visible_child (self->stack);
+
+  /*
+   * The tip of the backward jumplist could be very close to
+   * where we are now. So keep skipping backwards until the
+   * item isn't near our current position.
+   */
+
+  self->navigating++;
+
+  for (guint i = n_items; i > 0; i--)
+    {
+      g_autoptr(GbpHistoryItem) item = g_list_model_get_item (model, i - 1);
+
+      g_list_store_remove (self->back_store, i - 1);
+      g_list_store_insert (self->forward_store, 0, item);
+
+      if (!IDE_IS_EDITOR_VIEW (current) ||
+          !item_is_nearby (IDE_EDITOR_VIEW (current), item))
+        {
+          gbp_history_layout_stack_addin_navigate (self, item);
+          break;
+        }
+    }
+
+  self->navigating--;
+}
+
+static void
+move_next_edit_action (GSimpleAction *action,
+                       GVariant      *param,
+                       gpointer       user_data)
+{
+  GbpHistoryLayoutStackAddin *self = user_data;
+  IdeLayoutView *current;
+  GListModel *model;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (self));
+
+  model = G_LIST_MODEL (self->forward_store);
+  current = ide_layout_stack_get_visible_child (self->stack);
+
+  self->navigating++;
+
+  while (g_list_model_get_n_items (model) > 0)
+    {
+      g_autoptr(GbpHistoryItem) item = g_list_model_get_item (model, 0);
+
+      g_list_store_remove (self->forward_store, 0);
+      g_list_store_append (self->back_store, item);
+
+      if (!IDE_IS_EDITOR_VIEW (current) ||
+          !item_is_nearby (IDE_EDITOR_VIEW (current), item))
+        {
+          gbp_history_layout_stack_addin_navigate (self, item);
+          break;
+        }
+    }
+
+  self->navigating--;
+}
+
+static const GActionEntry entries[] = {
+  { "move-previous-edit", move_previous_edit_action },
+  { "move-next-edit", move_next_edit_action },
+};
+
+static void
+gbp_history_layout_stack_addin_load (IdeLayoutStackAddin *addin,
+                                     IdeLayoutStack      *stack)
+{
+  GbpHistoryLayoutStackAddin *self = (GbpHistoryLayoutStackAddin *)addin;
+  g_autoptr(GSimpleActionGroup) actions = NULL;
+  GtkWidget *header;
+
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (addin));
+  g_assert (IDE_IS_LAYOUT_STACK (stack));
+
+  self->stack = stack;
+
+  actions = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (actions),
+                                   entries,
+                                   G_N_ELEMENTS (entries),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (stack),
+                                  "history",
+                                  G_ACTION_GROUP (actions));
+
+  header = ide_layout_stack_get_titlebar (stack);
+
+  self->controls = g_object_new (GTK_TYPE_BOX,
+                                 "orientation", GTK_ORIENTATION_HORIZONTAL,
+                                 NULL);
+  g_signal_connect (self->controls,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->controls);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->controls), "linked");
+  gtk_container_add_with_properties (GTK_CONTAINER (header), GTK_WIDGET (self->controls),
+                                     "priority", -100,
+                                     NULL);
+
+  self->previous_button = g_object_new (GTK_TYPE_BUTTON,
+                                        "action-name", "history.move-previous-edit",
+                                        "child", g_object_new (GTK_TYPE_IMAGE,
+                                                               "icon-name", "go-previous-symbolic",
+                                                               "visible", TRUE,
+                                                               NULL),
+                                        "visible", TRUE,
+                                        NULL);
+  g_signal_connect (self->previous_button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->previous_button);
+  gtk_container_add (GTK_CONTAINER (self->controls), GTK_WIDGET (self->previous_button));
+
+  self->next_button = g_object_new (GTK_TYPE_BUTTON,
+                                    "action-name", "history.move-next-edit",
+                                    "child", g_object_new (GTK_TYPE_IMAGE,
+                                                           "icon-name", "go-next-symbolic",
+                                                           "visible", TRUE,
+                                                           NULL),
+                                    "visible", TRUE,
+                                    NULL);
+  g_signal_connect (self->next_button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->next_button);
+  gtk_container_add (GTK_CONTAINER (self->controls), GTK_WIDGET (self->next_button));
+
+  gbp_history_layout_stack_addin_update (self);
+}
+
+static void
+gbp_history_layout_stack_addin_unload (IdeLayoutStackAddin *addin,
+                                       IdeLayoutStack      *stack)
+{
+  GbpHistoryLayoutStackAddin *self = (GbpHistoryLayoutStackAddin *)addin;
+
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (addin));
+  g_assert (IDE_IS_LAYOUT_STACK (stack));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (stack), "history", NULL);
+
+  g_clear_object (&self->back_store);
+  g_clear_object (&self->forward_store);
+
+  if (self->controls != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->controls));
+  if (self->next_button != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->next_button));
+  if (self->previous_button != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->previous_button));
+
+  self->stack = NULL;
+}
+
+static void
+gbp_history_layout_stack_addin_set_view (IdeLayoutStackAddin *addin,
+                                         IdeLayoutView       *view)
+{
+  GbpHistoryLayoutStackAddin *self = (GbpHistoryLayoutStackAddin *)addin;
+
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (self));
+  g_assert (!view || IDE_IS_LAYOUT_VIEW (view));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->controls), IDE_IS_EDITOR_VIEW (view));
+}
+
+static void
+layout_stack_addin_iface_init (IdeLayoutStackAddinInterface *iface)
+{
+  iface->load = gbp_history_layout_stack_addin_load;
+  iface->unload = gbp_history_layout_stack_addin_unload;
+  iface->set_view = gbp_history_layout_stack_addin_set_view;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpHistoryLayoutStackAddin, gbp_history_layout_stack_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_LAYOUT_STACK_ADDIN,
+                                                layout_stack_addin_iface_init))
+
+static void
+gbp_history_layout_stack_addin_class_init (GbpHistoryLayoutStackAddinClass *klass)
+{
+}
+
+static void
+gbp_history_layout_stack_addin_init (GbpHistoryLayoutStackAddin *self)
+{
+  self->back_store = g_list_store_new (GBP_TYPE_HISTORY_ITEM);
+  self->forward_store = g_list_store_new (GBP_TYPE_HISTORY_ITEM);
+}
+
+static void
+move_forward_to_back_store (GbpHistoryLayoutStackAddin *self)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (self));
+
+  /* Be certain we're not disposed */
+  if (self->forward_store == NULL || self->back_store == NULL)
+    IDE_EXIT;
+
+  while (g_list_model_get_n_items (G_LIST_MODEL (self->forward_store)))
+    {
+      g_autoptr(GbpHistoryItem) item = NULL;
+
+      item = g_list_model_get_item (G_LIST_MODEL (self->forward_store), 0);
+      g_list_store_remove (self->forward_store, 0);
+      g_list_store_append (self->back_store, item);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+gbp_history_layout_stack_addin_remove_dups (GbpHistoryLayoutStackAddin *self)
+{
+  guint n_items;
+
+  g_assert (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (self));
+  g_assert (self->forward_store != NULL);
+  g_assert (g_list_model_get_n_items (G_LIST_MODEL (self->forward_store)) == 0);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->back_store));
+
+  /* Start from the oldest history item and work our way to the most
+   * recent item. Try to find any items later in the jump list which
+   * we can coallesce with. If so, remove the entry, preferring the
+   * more recent item.
+   */
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      GbpHistoryItem *item;
+
+    try_again:
+      item = g_list_model_get_item (G_LIST_MODEL (self->back_store), i);
+
+      for (guint j = n_items; (j - 1) > i; j--)
+        {
+          g_autoptr(GbpHistoryItem) recent = NULL;
+
+          recent = g_list_model_get_item (G_LIST_MODEL (self->back_store), j - 1);
+
+          g_assert (recent != item);
+
+          if (gbp_history_item_chain (recent, item))
+            {
+              g_list_store_remove (self->back_store, i);
+              g_object_unref (item);
+              n_items--;
+              goto try_again;
+            }
+        }
+
+      g_object_unref (item);
+    }
+}
+
+void
+gbp_history_layout_stack_addin_push (GbpHistoryLayoutStackAddin *self,
+                                     GbpHistoryItem             *item)
+{
+  guint n_items;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (GBP_IS_HISTORY_LAYOUT_STACK_ADDIN (self));
+  g_return_if_fail (GBP_IS_HISTORY_ITEM (item));
+  g_return_if_fail (self->back_store != NULL);
+  g_return_if_fail (self->forward_store != NULL);
+  g_return_if_fail (self->stack != NULL);
+
+  /* Ignore while we are navigating */
+  if (self->navigating != 0)
+    return;
+
+  /* Move all of our forward marks to the backward list */
+  move_forward_to_back_store (self);
+
+  /* Now add our new item to the list */
+  g_list_store_append (self->back_store, item);
+
+  /* Now remove dups in the list */
+  gbp_history_layout_stack_addin_remove_dups (self);
+
+  /* Truncate from head if necessary */
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->back_store));
+  if (n_items >= MAX_HISTORY_ITEMS)
+    g_list_store_remove (self->back_store, 0);
+
+  gbp_history_layout_stack_addin_update (self);
+
+  IDE_EXIT;
+}
diff --git a/plugins/history/gbp-history-layout-stack-addin.h 
b/plugins/history/gbp-history-layout-stack-addin.h
new file mode 100644
index 0000000..220ac36
--- /dev/null
+++ b/plugins/history/gbp-history-layout-stack-addin.h
@@ -0,0 +1,34 @@
+/* gbp-history-layout-stack-addin.h
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#pragma once
+
+#include <ide.h>
+
+#include "gbp-history-item.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_HISTORY_LAYOUT_STACK_ADDIN (gbp_history_layout_stack_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpHistoryLayoutStackAddin, gbp_history_layout_stack_addin, GBP, 
HISTORY_LAYOUT_STACK_ADDIN, GObject)
+
+void gbp_history_layout_stack_addin_push (GbpHistoryLayoutStackAddin *self,
+                                          GbpHistoryItem             *item);
+
+G_END_DECLS
diff --git a/plugins/history/gbp-history-plugin.c b/plugins/history/gbp-history-plugin.c
new file mode 100644
index 0000000..77bea6c
--- /dev/null
+++ b/plugins/history/gbp-history-plugin.c
@@ -0,0 +1,34 @@
+/* gbp-history-plugin.c
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#include <ide.h>
+#include <libpeas/peas.h>
+
+#include "gbp-history-editor-view-addin.h"
+#include "gbp-history-layout-stack-addin.h"
+
+void
+peas_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_VIEW_ADDIN,
+                                              GBP_TYPE_HISTORY_EDITOR_VIEW_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_LAYOUT_STACK_ADDIN,
+                                              GBP_TYPE_HISTORY_LAYOUT_STACK_ADDIN);
+}
diff --git a/plugins/history/history.plugin b/plugins/history/history.plugin
new file mode 100644
index 0000000..eca9f73
--- /dev/null
+++ b/plugins/history/history.plugin
@@ -0,0 +1,8 @@
+[Plugin]
+Module=history-plugin
+Name=Edit History
+Description=Tracks edits in buffer to provide navigation history
+Authors=Christian Hergert <christian hergert me>
+Copyright=Copyright © 2017 Christian Hergert
+Depends=editor
+Builtin=true
diff --git a/plugins/history/meson.build b/plugins/history/meson.build
new file mode 100644
index 0000000..c856566
--- /dev/null
+++ b/plugins/history/meson.build
@@ -0,0 +1,30 @@
+if get_option('with_history')
+
+history_sources = [
+  'gbp-history-layout-stack-addin.c',
+  'gbp-history-layout-stack-addin.h',
+  'gbp-history-editor-view-addin.c',
+  'gbp-history-editor-view-addin.h',
+  'gbp-history-item.c',
+  'gbp-history-item.h',
+  'gbp-history-plugin.c',
+]
+
+shared_module('history-plugin', history_sources,
+  dependencies: plugin_deps,
+     link_args: plugin_link_args,
+  link_depends: plugin_link_deps,
+       install: true,
+   install_dir: plugindir,
+ install_rpath: pkglibdir_abs,
+)
+
+configure_file(
+          input: 'history.plugin',
+         output: 'history.plugin',
+  configuration: configuration_data(),
+        install: true,
+    install_dir: plugindir,
+)
+
+endif
diff --git a/plugins/meson.build b/plugins/meson.build
index e9be9fd..320fb02 100644
--- a/plugins/meson.build
+++ b/plugins/meson.build
@@ -35,6 +35,7 @@ subdir('gdb')
 subdir('gettext')
 subdir('git')
 subdir('gnome-code-assistance')
+subdir('history')
 subdir('html-completion')
 subdir('html-preview')
 subdir('jedi')
@@ -90,6 +91,7 @@ status += [
   'Gettext ............... : @0@'.format(get_option('with_gettext')),
   'Git ................... : @0@'.format(get_option('with_git')),
   'GNOME Code Assistance . : @0@'.format(get_option('with_gnome_code_assistance')),
+  'History ............... : @0@'.format(get_option('with_history')),
   'HTML Completion ....... : @0@'.format(get_option('with_html_completion')),
   'HTML Preview .......... : @0@'.format(get_option('with_html_preview')),
   'Python Jedi ........... : @0@'.format(get_option('with_jedi')),



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