Commit 2e6dc3e6 authored by Christian Hergert's avatar Christian Hergert

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.
parent 978d98cc
......@@ -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) };
......
......@@ -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')
......
/* 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)
{
}
/* 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
/* 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;
}
/* 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
/* 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;