[gnome-builder] todo: port todo plugin to C and use new sidebar



commit bd29b18eb01a9cc6eebb2a6f5cc6c431ea68c02a
Author: Christian Hergert <chergert redhat com>
Date:   Fri Jun 30 17:48:36 2017 -0700

    todo: port todo plugin to C and use new sidebar
    
    This ports the TODO plugin to C as I was tired of tracking down crashes in
    PyGObject there (it's not necessarily to blame, but it was sufficiently
    difficult to find the real culprite when python threading is in play).
    
    Additionally, it tries to be more efficient with string storage than the
    previous version by using a large GByte buffer with \0 terminated strings
    inside that buffer. This allows us to have a single contiguous string
    buffer (and that is good for the allocator).
    
    Additionally, this switches to using the new IdeEditorSidebar.
    
    I decided to not take the time to implement line-wrapping like the current
    mockup has because doing that with CellRendererText is a bit obtuse. We
    previously tried to use GtkListStore for stuff like this (where we would
    get line wrapping for free) but it just wasn't tennable for lists over
    about 100. So this sticks with the performance model of GtkTreeView and
    just tries to get things close.

 plugins/todo/gbp-todo-item.c            |  152 ++++++++++
 plugins/todo/gbp-todo-item.h            |   41 +++
 plugins/todo/gbp-todo-model.c           |  489 +++++++++++++++++++++++++++++++
 plugins/todo/gbp-todo-model.h           |   39 +++
 plugins/todo/gbp-todo-panel.c           |  359 +++++++++++++++++++++++
 plugins/todo/gbp-todo-panel.h           |   35 +++
 plugins/todo/gbp-todo-plugin.c          |   30 ++
 plugins/todo/gbp-todo-workbench-addin.c |  172 +++++++++++
 plugins/todo/gbp-todo-workbench-addin.h |   29 ++
 plugins/todo/meson.build                |   20 ++-
 plugins/todo/todo.plugin                |   10 +-
 plugins/todo/todo_plugin/__init__.py    |  283 ------------------
 12 files changed, 1370 insertions(+), 289 deletions(-)
---
diff --git a/plugins/todo/gbp-todo-item.c b/plugins/todo/gbp-todo-item.c
new file mode 100644
index 0000000..d217c36
--- /dev/null
+++ b/plugins/todo/gbp-todo-item.c
@@ -0,0 +1,152 @@
+/* gbp-todo-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-todo-item"
+
+#include "gbp-todo-item.h"
+
+#define MAX_TODO_LINES 5
+
+struct _GbpTodoItem
+{
+  GObject      parent_instance;
+  GBytes      *bytes;
+  const gchar *path;
+  guint        lineno;
+  const gchar *lines[MAX_TODO_LINES];
+};
+
+G_DEFINE_TYPE (GbpTodoItem, gbp_todo_item, G_TYPE_OBJECT)
+
+static void
+gbp_todo_item_finalize (GObject *object)
+{
+  GbpTodoItem *self = (GbpTodoItem *)object;
+
+  g_clear_pointer (&self->bytes, g_bytes_unref);
+
+  G_OBJECT_CLASS (gbp_todo_item_parent_class)->finalize (object);
+}
+
+static void
+gbp_todo_item_class_init (GbpTodoItemClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_todo_item_finalize;
+}
+
+static void
+gbp_todo_item_init (GbpTodoItem *self)
+{
+}
+
+/**
+ * gbp_todo_item_new:
+ * @bytes: a buffer containing all the subsequent data
+ *
+ * This creates a new #GbpTodoItem.
+ *
+ * Getting the parameters right to this function is essential.
+ *
+ * @bytes should contain a buffer that can be used to access
+ * the raw string pointers used in later API calls to this
+ * object.
+ *
+ * This is done to avoid fragmentation due to lots of
+ * small string allocations.
+ *
+ * Returns: (transfer full): A newly allocated #GbpTodoItem
+ *
+ * Since: 3.26
+ */
+GbpTodoItem *
+gbp_todo_item_new (GBytes *bytes)
+{
+  GbpTodoItem *self;
+
+  g_assert (bytes != NULL);
+
+  self = g_object_new (GBP_TYPE_TODO_ITEM, NULL);
+  self->bytes = g_bytes_ref (bytes);
+
+  return self;
+}
+
+void
+gbp_todo_item_add_line (GbpTodoItem *self,
+                        const gchar *line)
+{
+  g_assert (GBP_IS_TODO_ITEM (self));
+  g_assert (line != NULL);
+
+  for (guint i = 0; i < G_N_ELEMENTS (self->lines); i++)
+    {
+      if (self->lines[i] == NULL)
+        {
+          self->lines[i] = line;
+          break;
+        }
+    }
+}
+
+const gchar *
+gbp_todo_item_get_line (GbpTodoItem *self,
+                        guint        nth)
+{
+  g_return_val_if_fail (GBP_IS_TODO_ITEM (self), NULL);
+
+  if (nth < G_N_ELEMENTS (self->lines))
+    return self->lines[nth];
+
+  return NULL;
+}
+
+guint
+gbp_todo_item_get_lineno (GbpTodoItem *self)
+{
+  g_return_val_if_fail (GBP_IS_TODO_ITEM (self), 0);
+
+  return self->lineno;
+}
+
+void
+gbp_todo_item_set_lineno (GbpTodoItem *self,
+                          guint        lineno)
+{
+  g_return_if_fail (GBP_IS_TODO_ITEM (self));
+
+  self->lineno = lineno;
+}
+
+const gchar *
+gbp_todo_item_get_path (GbpTodoItem *self)
+{
+  g_return_val_if_fail (GBP_IS_TODO_ITEM (self), NULL);
+
+  return self->path;
+}
+
+void
+gbp_todo_item_set_path (GbpTodoItem *self,
+                        const gchar *path)
+{
+  g_return_if_fail (GBP_IS_TODO_ITEM (self));
+
+  self->path = path;
+}
diff --git a/plugins/todo/gbp-todo-item.h b/plugins/todo/gbp-todo-item.h
new file mode 100644
index 0000000..11e9c3c
--- /dev/null
+++ b/plugins/todo/gbp-todo-item.h
@@ -0,0 +1,41 @@
+/* gbp-todo-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 <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_TODO_ITEM (gbp_todo_item_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTodoItem, gbp_todo_item, GBP, TODO_ITEM, GObject)
+
+GbpTodoItem *gbp_todo_item_new        (GBytes       *bytes);
+const gchar *gbp_todo_item_get_path   (GbpTodoItem  *self);
+void         gbp_todo_item_set_path   (GbpTodoItem  *self,
+                                       const gchar  *path);
+void         gbp_todo_item_set_lineno (GbpTodoItem  *self,
+                                       guint         lineno);
+guint        gbp_todo_item_get_lineno (GbpTodoItem  *self);
+void         gbp_todo_item_add_line   (GbpTodoItem  *self,
+                                       const gchar  *line);
+const gchar *gbp_todo_item_get_line   (GbpTodoItem  *self,
+                                       guint         nth);
+
+G_END_DECLS
diff --git a/plugins/todo/gbp-todo-model.c b/plugins/todo/gbp-todo-model.c
new file mode 100644
index 0000000..8b7dfad
--- /dev/null
+++ b/plugins/todo/gbp-todo-model.c
@@ -0,0 +1,489 @@
+/* gbp-todo-model.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-todo-model"
+
+#include <ide.h>
+#include <string.h>
+
+#include "gbp-todo-model.h"
+#include "gbp-todo-item.h"
+
+/*
+ * If you feel like optimizing this, I would go the route of creating a custom
+ * GtkTreeModelIface. Unfortunately GtkTreeDataList always copies strings from
+ * the caller (even when using gtk_list_store_set_value() w/ static string) so
+ * that prevents us from being clever about using GStringChunk/etc.
+ *
+ * My preference would be a 2-level tree, with the first level being the index
+ * of files, and the second level being the items, with string pointers into
+ * a GStringChunk. Most things wont change often, so that space for strings,
+ * even when deleted, is more than fine.
+ */
+
+struct _GbpTodoModel {
+  GtkListStore parent_instance;
+};
+
+typedef struct
+{
+  GbpTodoModel *self;
+  GPtrArray    *items;
+} ResultInfo;
+
+G_DEFINE_TYPE (GbpTodoModel, gbp_todo_model, GTK_TYPE_LIST_STORE)
+
+static GRegex *line1;
+static GRegex *line2;
+
+static const gchar *exclude_dirs[] = {
+  ".bzr",
+  ".flatpak-builder",
+  ".git",
+  ".svn",
+};
+
+static const gchar *exclude_files[] = {
+  "*.m4",
+  "*.po",
+};
+
+static const gchar *keywords[] = {
+  "FIXME",
+  "XXX",
+  "TODO",
+  "HACK",
+};
+
+static void
+result_info_free (gpointer data)
+{
+  ResultInfo *info = data;
+
+  g_clear_object (&info->self);
+  g_clear_pointer (&info->items, g_ptr_array_unref);
+  g_slice_free (ResultInfo, info);
+}
+
+static void
+gbp_todo_model_clear (GbpTodoModel *self,
+                      const gchar  *path)
+{
+  GtkTreeIter iter;
+  gboolean matched = FALSE;
+
+  g_assert (GBP_IS_TODO_MODEL (self));
+  g_assert (path != NULL);
+
+  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self), &iter))
+    {
+      do
+        {
+          g_autoptr(GbpTodoItem) item = NULL;
+          const gchar *item_path;
+
+        again:
+
+          gtk_tree_model_get (GTK_TREE_MODEL (self), &iter, 0, &item, -1);
+          item_path = gbp_todo_item_get_path (item);
+
+          if (g_strcmp0 (path, item_path) == 0)
+            {
+              if (!gtk_list_store_remove (GTK_LIST_STORE (self), &iter))
+                break;
+              matched = TRUE;
+              g_clear_object (&item);
+              goto again;
+            }
+          else if (matched)
+            {
+              /* We skipped past our last match, so we might as well
+               * short-circuit and avoid looking at more rows.
+               */
+              break;
+            }
+        }
+      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self), &iter));
+    }
+}
+
+static void
+gbp_todo_model_merge (GbpTodoModel *self,
+                      GbpTodoItem  *item)
+{
+  GtkTreeIter iter;
+
+  g_assert (GBP_IS_TODO_MODEL (self));
+  g_assert (GBP_IS_TODO_ITEM (item));
+
+  gtk_list_store_prepend (GTK_LIST_STORE (self), &iter);
+  gtk_list_store_set (GTK_LIST_STORE (self), &iter, 0, item, -1);
+}
+
+static gboolean
+gbp_todo_model_merge_results (gpointer user_data)
+{
+  ResultInfo *info = user_data;
+  const gchar *last_path = NULL;
+  gboolean needs_clear = FALSE;
+
+  g_assert (info != NULL);
+  g_assert (GBP_IS_TODO_MODEL (info->self));
+  g_assert (info->items != NULL);
+
+  /* Try to avoid clearing on the initial build of the model */
+  if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (info->self), NULL) > 0)
+    needs_clear = TRUE;
+
+  /* Walk backwards to preserve ordering, as merging will always prepend
+   * the item to the store.
+   */
+  for (guint i = info->items->len; i > 0; i--)
+    {
+      GbpTodoItem *item = g_ptr_array_index (info->items, i - 1);
+      const gchar *path = gbp_todo_item_get_path (item);
+
+      if (needs_clear && (g_strcmp0 (path, last_path) != 0))
+        gbp_todo_model_clear (info->self, path);
+
+      gbp_todo_model_merge (info->self, item);
+
+      last_path = path;
+    }
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_todo_model_class_init (GbpTodoModelClass *klass)
+{
+  g_autoptr(GError) error = NULL;
+
+  line1 = g_regex_new ("(.*):(\\d+):(.*)", 0, 0, &error);
+  g_assert_no_error (error);
+  g_assert (line1 != NULL);
+
+  line2 = g_regex_new ("(.*)-(\\d+)-(.*)", 0, 0, &error);
+  g_assert_no_error (error);
+  g_assert (line2 != NULL);
+}
+
+static void
+gbp_todo_model_init (GbpTodoModel *self)
+{
+  GType column_types[] = { GBP_TYPE_TODO_ITEM };
+
+  gtk_list_store_set_column_types (GTK_LIST_STORE (self),
+                                   G_N_ELEMENTS (column_types),
+                                   column_types);
+}
+
+/**
+ * gbp_todo_model_new:
+ *
+ * Creates a new #GbpTodoModel.
+ *
+ * Returns: (transfer full): A newly created #GbpTodoModel.
+ *
+ * Since: 3.26
+ */
+GbpTodoModel *
+gbp_todo_model_new (void)
+{
+  return g_object_new (GBP_TYPE_TODO_MODEL, NULL);
+}
+
+static void
+gbp_todo_model_mine_worker (GTask        *task,
+                            gpointer      source_object,
+                            gpointer      task_data,
+                            GCancellable *cancellable)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GPtrArray) items = NULL;
+  g_autoptr(GbpTodoItem) item = NULL;
+  g_autoptr(GBytes) bytes = NULL;
+  g_autoptr(GTimer) timer = g_timer_new ();
+  g_autofree gchar *path = NULL;
+  IdeLineReader reader;
+  ResultInfo *info;
+  gchar *stdoutstr = NULL;
+  GFile *file = task_data;
+  gchar *line;
+  gsize stdoutstr_len;
+  gsize pathlen;
+  gsize len;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (GBP_IS_TODO_MODEL (source_object));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE);
+
+  ide_subprocess_launcher_push_argv (launcher, "grep");
+  ide_subprocess_launcher_push_argv (launcher, "-A");
+  ide_subprocess_launcher_push_argv (launcher, "5");
+  ide_subprocess_launcher_push_argv (launcher, "-I");
+  ide_subprocess_launcher_push_argv (launcher, "-H");
+  ide_subprocess_launcher_push_argv (launcher, "-n");
+  ide_subprocess_launcher_push_argv (launcher, "-r");
+  ide_subprocess_launcher_push_argv (launcher, "-E");
+
+  for (guint i = 0; i < G_N_ELEMENTS (exclude_files); i++)
+    {
+      const gchar *exclude_file = exclude_files[i];
+      g_autofree gchar *arg = NULL;
+
+      arg = g_strdup_printf ("--exclude=%s", exclude_file);
+      ide_subprocess_launcher_push_argv (launcher, arg);
+    }
+
+  for (guint i = 0; i < G_N_ELEMENTS (exclude_dirs); i++)
+    {
+      const gchar *exclude_dir = exclude_dirs[i];
+      g_autofree gchar *arg = NULL;
+
+      arg = g_strdup_printf ("--exclude-dir=%s", exclude_dir);
+      ide_subprocess_launcher_push_argv (launcher, arg);
+    }
+
+  for (guint i = 0; i < G_N_ELEMENTS (keywords); i++)
+    {
+      const gchar *keyword = keywords[i];
+      g_autofree gchar *arg = NULL;
+
+      arg = g_strdup_printf ("%s(:| )", keyword);
+      ide_subprocess_launcher_push_argv (launcher, "-e");
+      ide_subprocess_launcher_push_argv (launcher, arg);
+    }
+
+  /* Let grep know where to scan */
+  path = g_file_get_path (file);
+  pathlen = strlen (path);
+  ide_subprocess_launcher_push_argv (launcher, path);
+
+  /* Spawn our grep process */
+  if (NULL == (subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error)))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  /* Read all of the output into a giant string */
+  if (!ide_subprocess_communicate_utf8 (subprocess, NULL, NULL, &stdoutstr, NULL, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  /*
+   * To avoid lots of string allocations in the model, we instead
+   * store GObjects which contain a reference to the whole buffer
+   * (the GBytes) and raw pointers into that data. We'll create
+   * the buffer up front, but still mutate the contents as we
+   * walk through it.
+   */
+  stdoutstr_len = strlen (stdoutstr);
+  bytes = g_bytes_new_take (stdoutstr, stdoutstr_len);
+
+  items = g_ptr_array_new_with_free_func (g_object_unref);
+
+  /*
+   * Process the STDOUT string iteratively, while trying to be tidy
+   * with additional allocations. If we overwrite the trailing \n to
+   * a \0, we can use it as a C string.
+   */
+  ide_line_reader_init (&reader, stdoutstr, stdoutstr_len);
+  while (NULL != (line = ide_line_reader_next (&reader, &len)))
+    {
+      g_autoptr(GMatchInfo) match_info1 = NULL;
+      g_autoptr(GMatchInfo) match_info2 = NULL;
+
+      line[len] = '\0';
+
+      /* We just finished a group of lines, flush this item
+       * to the list of items and get started processing the
+       * next result.
+       */
+      if (g_str_has_prefix (line, "--"))
+        {
+          if (item != NULL)
+            g_ptr_array_add (items, g_steal_pointer (&item));
+          continue;
+        }
+
+      if (ide_str_empty0 (line))
+        continue;
+
+      /* Try to match the first line */
+      if (item == NULL)
+        {
+          if (g_regex_match_full (line1, line, len, 0, 0, &match_info1, NULL))
+            {
+              gint begin;
+              gint end;
+
+              item = gbp_todo_item_new (bytes);
+
+              /* Get the path */
+              if (g_match_info_fetch_pos (match_info1, 1, &begin, &end))
+                {
+                  const gchar *pathstr;
+
+                  line[end] = '\0';
+                  pathstr = &line[begin];
+
+                  /*
+                   * Try to skip past the prefix of the working directory
+                   * of the project.
+                   */
+
+                  if (strncmp (pathstr, path, pathlen) == 0)
+                    {
+                      pathstr += pathlen;
+
+                      while (*pathstr == G_DIR_SEPARATOR)
+                        pathstr++;
+                    }
+
+                  gbp_todo_item_set_path (item, pathstr);
+                }
+
+              /* Get the line number */
+              if (g_match_info_fetch_pos (match_info1, 2, &begin, &end))
+                {
+                  gint64 lineno;
+
+                  line[end] = '\0';
+                  lineno = g_ascii_strtoll (&line[begin], NULL, 10);
+                  gbp_todo_item_set_lineno (item, lineno);
+                }
+
+              /* Get the message */
+              if (g_match_info_fetch_pos (match_info1, 3, &begin, &end))
+                {
+                  line[end] = '\0';
+                  gbp_todo_item_add_line (item, &line[begin]);
+                }
+            }
+
+          continue;
+        }
+
+      g_assert (item != NULL);
+
+      if (g_regex_match_full (line2, line, len, 0, 0, &match_info2, NULL))
+        {
+          gint begin;
+          gint end;
+
+          /* Get the message */
+          if (g_match_info_fetch_pos (match_info2, 3, &begin, &end))
+            {
+              line[end] = '\0';
+              gbp_todo_item_add_line (item, &line[begin]);
+            }
+        }
+    }
+
+  g_debug ("Located %u TODO items in %0.4lf seconds",
+           items->len, g_timer_elapsed (timer, NULL));
+
+  info = g_slice_new0 (ResultInfo);
+  info->self = g_object_ref (source_object);
+  info->items = g_steal_pointer (&items);
+
+  gdk_threads_add_idle_full (G_PRIORITY_LOW + 100,
+                             gbp_todo_model_merge_results,
+                             info, result_info_free);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+/**
+ * gbp_todo_model_mine_async:
+ * @self: a #GbpTodoModel
+ * @file: A #GFile to mine
+ * @cancellable: (nullable): A #Gancellable or %NULL
+ * @callback: (scope async) (closure user_data): An async callback
+ * @user_data: user data for @callback
+ *
+ * Asynchronously mines @file.
+ *
+ * If @file is a directory, it will be recursively scanned.  @callback
+ * will be called after the operation is complete.  Call
+ * gbp_todo_model_mine_finish() to get the result of this operation.
+ *
+ * If @file is not a native file (meaning it is accessable on the
+ * normal, mounted, local file-system) this operation will fail.
+ *
+ * Since: 3.26
+ */
+void
+gbp_todo_model_mine_async (GbpTodoModel        *self,
+                           GFile               *file,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (GBP_IS_TODO_MODEL (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_priority (task, G_PRIORITY_LOW + 100);
+  g_task_set_source_tag (task, gbp_todo_model_mine_async);
+  g_task_set_task_data (task, g_object_ref (file), g_object_unref);
+
+  if (!g_file_is_native (file))
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_SUPPORTED,
+                               "Only local files are supported");
+      return;
+    }
+
+  g_task_run_in_thread (task, gbp_todo_model_mine_worker);
+}
+
+/**
+ * gbp_todo_model_mine_finish:
+ * @self: a #GbpTodoModel
+ * @result: A #GAsyncResult
+ * @error: A location for a #GError or %NULL
+ *
+ * Completes an asynchronous request to gbp_todo_model_mine_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ */
+gboolean
+gbp_todo_model_mine_finish (GbpTodoModel  *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_return_val_if_fail (GBP_IS_TODO_MODEL (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/plugins/todo/gbp-todo-model.h b/plugins/todo/gbp-todo-model.h
new file mode 100644
index 0000000..e88495e
--- /dev/null
+++ b/plugins/todo/gbp-todo-model.h
@@ -0,0 +1,39 @@
+/* gbp-todo-model.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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_TODO_MODEL (gbp_todo_model_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTodoModel, gbp_todo_model, GBP, TODO_MODEL, GtkListStore)
+
+GbpTodoModel *gbp_todo_model_new         (void);
+void          gbp_todo_model_mine_async  (GbpTodoModel         *self,
+                                          GFile                *file,
+                                          GCancellable         *cancellable,
+                                          GAsyncReadyCallback   callback,
+                                          gpointer              user_data);
+gboolean      gbp_todo_model_mine_finish (GbpTodoModel         *self,
+                                          GAsyncResult         *result,
+                                          GError              **error);
+
+G_END_DECLS
diff --git a/plugins/todo/gbp-todo-panel.c b/plugins/todo/gbp-todo-panel.c
new file mode 100644
index 0000000..11536ca
--- /dev/null
+++ b/plugins/todo/gbp-todo-panel.c
@@ -0,0 +1,359 @@
+/* gbp-todo-panel.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-todo-panel"
+
+#include <ide.h>
+
+#include "gbp-todo-item.h"
+#include "gbp-todo-panel.h"
+
+struct _GbpTodoPanel
+{
+  GtkBin        parent_instance;
+
+  GtkTreeView  *tree_view;
+  GbpTodoModel *model;
+};
+
+G_DEFINE_TYPE (GbpTodoPanel, gbp_todo_panel, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_todo_panel_cell_data_func (GtkCellLayout   *cell_layout,
+                               GtkCellRenderer *cell,
+                               GtkTreeModel    *tree_model,
+                               GtkTreeIter     *iter,
+                               gpointer         data)
+{
+  g_autoptr(GbpTodoItem) item = NULL;
+  g_autofree gchar *markup = NULL;
+  const gchar *message;
+
+  gtk_tree_model_get (tree_model, iter, 0, &item, -1);
+
+  message = gbp_todo_item_get_line (item, 0);
+
+  if (message != NULL)
+    {
+      g_autofree gchar *escape1 = NULL;
+      g_autofree gchar *escape2 = NULL;
+      guint lineno;
+
+      /*
+       * We don't trim the whitespace from lines so that we can keep
+       * them in tact when showing tooltips. So we need to truncate
+       * here for display in the pane.
+       */
+      while (g_ascii_isspace (*message))
+        message++;
+
+      escape1 = g_markup_escape_text (gbp_todo_item_get_path (item), -1);
+      escape2 = g_markup_escape_text (message, -1);
+      lineno = gbp_todo_item_get_lineno (item);
+
+      markup = g_strdup_printf ("<span size='smaller' fgalpha='32768'>%s:%u</span>\n%s",
+                                escape1, lineno, escape2);
+    }
+
+  g_object_set (cell, "markup", markup, NULL);
+}
+
+static void
+gbp_todo_panel_row_activated (GbpTodoPanel      *self,
+                              GtkTreePath       *tree_path,
+                              GtkTreeViewColumn *column,
+                              GtkTreeView       *tree_view)
+{
+  g_autoptr(GbpTodoItem) item = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(IdeUri) uri = NULL;
+  g_autofree gchar *fragment = NULL;
+  IdeWorkbench *workbench;
+  GtkTreeModel *model;
+  const gchar *path;
+  GtkTreeIter iter;
+  guint lineno;
+
+  g_assert (GBP_IS_TODO_PANEL (self));
+  g_assert (tree_path != NULL);
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  model = gtk_tree_view_get_model (tree_view);
+  gtk_tree_model_get_iter (model, &iter, tree_path);
+  gtk_tree_model_get (model, &iter, 0, &item, -1);
+  g_assert (GBP_IS_TODO_ITEM (item));
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  path = gbp_todo_item_get_path (item);
+  g_assert (path != NULL);
+
+  if (g_path_is_absolute (path))
+    {
+      file = g_file_new_for_path (path);
+    }
+  else
+    {
+      IdeContext *context;
+      IdeVcs *vcs;
+      GFile *workdir;
+
+      context = ide_workbench_get_context (workbench);
+      vcs = ide_context_get_vcs (context);
+      workdir = ide_vcs_get_working_directory (vcs);
+      file = g_file_get_child (workdir, path);
+    }
+
+  uri = ide_uri_new_from_file (file);
+
+  /* Set lineno info so that the editor can jump
+   * to the location of the TODO item.
+   */
+  lineno = gbp_todo_item_get_lineno (item);
+  fragment = g_strdup_printf ("L%u", lineno);
+  ide_uri_set_fragment (uri, fragment);
+
+  ide_workbench_open_uri_async (workbench, uri, "editor", 0, NULL, NULL, NULL);
+}
+
+static gboolean
+gbp_todo_panel_query_tooltip (GbpTodoPanel *self,
+                              gint          x,
+                              gint          y,
+                              gboolean      keyboard_mode,
+                              GtkTooltip   *tooltip,
+                              GtkTreeView  *tree_view)
+{
+  GtkTreePath *path = NULL;
+  GtkTreeModel *model;
+
+  g_assert (GBP_IS_TODO_PANEL (self));
+  g_assert (GTK_IS_TOOLTIP (tooltip));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  if (NULL == (model = gtk_tree_view_get_model (tree_view)))
+    return FALSE;
+
+  if (gtk_tree_view_get_path_at_pos (tree_view, x, y, &path, NULL, NULL, NULL))
+    {
+      GtkTreeIter iter;
+
+      if (gtk_tree_model_get_iter (model, &iter, path))
+        {
+          g_autoptr(GbpTodoItem) item = NULL;
+          g_autoptr(GString) str = g_string_new ("<tt>");
+
+          gtk_tree_model_get (model, &iter, 0, &item, -1);
+          g_assert (GBP_IS_TODO_ITEM (item));
+
+          /* only 5 lines stashed */
+          for (guint i = 0; i < 5; i++)
+            {
+              const gchar *line = gbp_todo_item_get_line (item, i);
+              g_autofree gchar *escaped = NULL;
+
+              if (!line)
+                break;
+
+              escaped = g_markup_escape_text (line, -1);
+              g_string_append (str, escaped);
+              g_string_append_c (str, '\n');
+            }
+
+          g_string_append (str, "</tt>");
+          gtk_tooltip_set_markup (tooltip, str->str);
+        }
+
+      gtk_tree_path_free (path);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+gbp_todo_panel_destroy (GtkWidget *widget)
+{
+  GbpTodoPanel *self = (GbpTodoPanel *)widget;
+
+  g_assert (GBP_IS_TODO_PANEL (self));
+
+  if (self->tree_view != NULL)
+    gtk_tree_view_set_model (self->tree_view, NULL);
+
+  g_clear_object (&self->model);
+
+  GTK_WIDGET_CLASS (gbp_todo_panel_parent_class)->destroy (widget);
+}
+
+static void
+gbp_todo_panel_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  GbpTodoPanel *self = GBP_TODO_PANEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, gbp_todo_panel_get_model (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_todo_panel_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  GbpTodoPanel *self = GBP_TODO_PANEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      gbp_todo_panel_set_model (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_todo_panel_class_init (GbpTodoPanelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = gbp_todo_panel_get_property;
+  object_class->set_property = gbp_todo_panel_set_property;
+
+  widget_class->destroy = gbp_todo_panel_destroy;
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model",
+                         "Model",
+                         "The model for the TODO list",
+                         GBP_TYPE_TODO_MODEL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_todo_panel_init (GbpTodoPanel *self)
+{
+  GtkWidget *scroller;
+  GtkTreeViewColumn *column;
+  GtkCellRenderer *cell;
+
+  scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
+                           "visible", TRUE,
+                           "vexpand", TRUE,
+                           NULL);
+  gtk_container_add (GTK_CONTAINER (self), scroller);
+
+  self->tree_view = g_object_new (GTK_TYPE_TREE_VIEW,
+                                  "activate-on-single-click", TRUE,
+                                  "has-tooltip", TRUE,
+                                  "headers-visible", FALSE,
+                                  "visible", TRUE,
+                                  NULL);
+  g_signal_connect (self->tree_view,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->tree_view);
+  g_signal_connect_swapped (self->tree_view,
+                            "row-activated",
+                            G_CALLBACK (gbp_todo_panel_row_activated),
+                            self);
+  g_signal_connect_swapped (self->tree_view,
+                            "query-tooltip",
+                            G_CALLBACK (gbp_todo_panel_query_tooltip),
+                            self);
+  gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (self->tree_view));
+
+  column = g_object_new (GTK_TYPE_TREE_VIEW_COLUMN,
+                         "expand", TRUE,
+                         "visible", TRUE,
+                         NULL);
+  gtk_tree_view_append_column (self->tree_view, column);
+
+  cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
+                       "ellipsize", PANGO_ELLIPSIZE_END,
+                       "visible", TRUE,
+                       "xalign", 0.0f,
+                       "xpad", 3,
+                       "ypad", 6,
+                       NULL);
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column),
+                                      cell,
+                                      gbp_todo_panel_cell_data_func,
+                                      NULL, NULL);
+}
+
+/**
+ * gbp_todo_panel_get_model:
+ * @self: a #GbpTodoPanel
+ *
+ * Gets the model being displayed by the treeview.
+ *
+ * Returns: (transfer none) (nullable): A #GbpTodoModel.
+ */
+GbpTodoModel *
+gbp_todo_panel_get_model (GbpTodoPanel *self)
+{
+  g_return_val_if_fail (GBP_IS_TODO_PANEL (self), NULL);
+
+  return self->model;
+}
+
+void
+gbp_todo_panel_set_model (GbpTodoPanel *self,
+                          GbpTodoModel *model)
+{
+  g_return_if_fail (GBP_IS_TODO_PANEL (self));
+  g_return_if_fail (!model || GBP_IS_TODO_MODEL (model));
+
+  if (g_set_object (&self->model, model))
+    {
+      if (self->model != NULL)
+        gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (self->model));
+      else
+        gtk_tree_view_set_model (self->tree_view, NULL);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
+    }
+}
diff --git a/plugins/todo/gbp-todo-panel.h b/plugins/todo/gbp-todo-panel.h
new file mode 100644
index 0000000..66041e9
--- /dev/null
+++ b/plugins/todo/gbp-todo-panel.h
@@ -0,0 +1,35 @@
+/* gbp-todo-panel.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-todo-model.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_TODO_PANEL (gbp_todo_panel_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTodoPanel, gbp_todo_panel, GBP, TODO_PANEL, GtkBin)
+
+GbpTodoModel *gbp_todo_panel_get_model (GbpTodoPanel *self);
+void          gbp_todo_panel_set_model (GbpTodoPanel *self,
+                                        GbpTodoModel *model);
+
+G_END_DECLS
diff --git a/plugins/todo/gbp-todo-plugin.c b/plugins/todo/gbp-todo-plugin.c
new file mode 100644
index 0000000..5c7fd01
--- /dev/null
+++ b/plugins/todo/gbp-todo-plugin.c
@@ -0,0 +1,30 @@
+/* gbp-todo-plugin.c
+ *
+ * Copyright (C) 2015-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 <libpeas/peas.h>
+#include <ide.h>
+
+#include "gbp-todo-workbench-addin.h"
+
+void
+peas_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_TODO_WORKBENCH_ADDIN);
+}
diff --git a/plugins/todo/gbp-todo-workbench-addin.c b/plugins/todo/gbp-todo-workbench-addin.c
new file mode 100644
index 0000000..60ef645
--- /dev/null
+++ b/plugins/todo/gbp-todo-workbench-addin.c
@@ -0,0 +1,172 @@
+/* gbp-todo-workbench-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-todo-workbench-addin"
+
+#include <glib/gi18n.h>
+
+#include "gbp-todo-workbench-addin.h"
+#include "gbp-todo-panel.h"
+
+struct _GbpTodoWorkbenchAddin
+{
+  GObject       parent_instance;
+  GbpTodoPanel *panel;
+  GbpTodoModel *model;
+  GCancellable *cancellable;
+};
+
+static void
+gbp_todo_workbench_addin_mine_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  GbpTodoModel *model = (GbpTodoModel *)object;
+  g_autoptr(GbpTodoWorkbenchAddin) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_TODO_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_TODO_MODEL (model));
+
+  if (!gbp_todo_model_mine_finish (model, result, &error))
+    g_warning ("%s", error->message);
+}
+
+static void
+gbp_todo_workbench_addin_buffer_saved (GbpTodoWorkbenchAddin *self,
+                                       IdeBuffer             *buffer,
+                                       IdeBufferManager      *bufmgr)
+{
+  IdeFile *file;
+  GFile *gfile;
+
+  g_assert (GBP_IS_TODO_WORKBENCH_ADDIN (self));
+  g_assert (self->model != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+
+  file = ide_buffer_get_file (buffer);
+  gfile = ide_file_get_file (file);
+  gbp_todo_model_mine_async (self->model,
+                             gfile,
+                             self->cancellable,
+                             gbp_todo_workbench_addin_mine_cb,
+                             g_object_ref (self));
+}
+
+static void
+gbp_todo_workbench_addin_load (IdeWorkbenchAddin *addin,
+                               IdeWorkbench      *workbench)
+{
+  GbpTodoWorkbenchAddin *self = (GbpTodoWorkbenchAddin *)addin;
+  IdeEditorSidebar *sidebar;
+  IdeBufferManager *bufmgr;
+  IdePerspective *editor;
+  IdeContext *context;
+  IdeVcs *vcs;
+  GFile *workdir;
+
+  g_assert (GBP_IS_TODO_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  self->cancellable = g_cancellable_new ();
+
+  context = ide_workbench_get_context (workbench);
+  vcs = ide_context_get_vcs (context);
+  workdir = ide_vcs_get_working_directory (vcs);
+  bufmgr = ide_context_get_buffer_manager (context);
+  editor = ide_workbench_get_perspective_by_name (workbench, "editor");
+  sidebar = ide_editor_perspective_get_sidebar (IDE_EDITOR_PERSPECTIVE (editor));
+
+  g_signal_connect_object (bufmgr,
+                           "buffer-saved",
+                           G_CALLBACK (gbp_todo_workbench_addin_buffer_saved),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  self->model = gbp_todo_model_new ();
+
+  self->panel = g_object_new (GBP_TYPE_TODO_PANEL,
+                              "model", self->model,
+                              "visible", TRUE,
+                              NULL);
+  g_signal_connect (self->panel,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->panel);
+  ide_editor_sidebar_add_section (sidebar,
+                                  "todo",
+                                  _("TODO/FIXMEs"),
+                                  "emblem-ok-symbolic",
+                                  NULL, NULL,
+                                  GTK_WIDGET (self->panel));
+
+  gbp_todo_model_mine_async (self->model,
+                             workdir,
+                             self->cancellable,
+                             gbp_todo_workbench_addin_mine_cb,
+                             g_object_ref (self));
+}
+
+static void
+gbp_todo_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                 IdeWorkbench      *workbench)
+{
+  GbpTodoWorkbenchAddin *self = (GbpTodoWorkbenchAddin *)addin;
+  IdeBufferManager *bufmgr;
+  IdeContext *context;
+
+  g_assert (GBP_IS_TODO_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  context = ide_workbench_get_context (workbench);
+  bufmgr = ide_context_get_buffer_manager (context);
+
+  g_signal_handlers_disconnect_by_func (bufmgr,
+                                        G_CALLBACK (gbp_todo_workbench_addin_buffer_saved),
+                                        self);
+
+  gtk_widget_destroy (GTK_WIDGET (self->panel));
+
+  g_clear_object (&self->model);
+}
+
+static void
+workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load = gbp_todo_workbench_addin_load;
+  iface->unload = gbp_todo_workbench_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpTodoWorkbenchAddin, gbp_todo_workbench_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN,
+                                                workbench_addin_iface_init))
+
+static void
+gbp_todo_workbench_addin_class_init (GbpTodoWorkbenchAddinClass *klass)
+{
+}
+
+static void
+gbp_todo_workbench_addin_init (GbpTodoWorkbenchAddin *self)
+{
+}
diff --git a/plugins/todo/gbp-todo-workbench-addin.h b/plugins/todo/gbp-todo-workbench-addin.h
new file mode 100644
index 0000000..086ba17
--- /dev/null
+++ b/plugins/todo/gbp-todo-workbench-addin.h
@@ -0,0 +1,29 @@
+/* gbp-todo-workbench-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_TODO_WORKBENCH_ADDIN (gbp_todo_workbench_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTodoWorkbenchAddin, gbp_todo_workbench_addin, GBP, TODO_WORKBENCH_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/plugins/todo/meson.build b/plugins/todo/meson.build
index 7bb146e..d145ee8 100644
--- a/plugins/todo/meson.build
+++ b/plugins/todo/meson.build
@@ -1,6 +1,24 @@
 if get_option('with_todo')
 
-install_subdir('todo_plugin', install_dir: plugindir)
+todo_sources = [
+  'gbp-todo-item.c',
+  'gbp-todo-item.h',
+  'gbp-todo-model.c',
+  'gbp-todo-model.h',
+  'gbp-todo-plugin.c',
+  'gbp-todo-panel.c',
+  'gbp-todo-panel.h',
+  'gbp-todo-workbench-addin.c',
+  'gbp-todo-workbench-addin.h',
+]
+
+shared_module('todo-plugin', todo_sources,
+  dependencies: plugin_deps,
+     link_args: plugin_link_args,
+  link_depends: plugin_link_deps,
+       install: true,
+   install_dir: plugindir,
+)
 
 configure_file(
           input: 'todo.plugin',
diff --git a/plugins/todo/todo.plugin b/plugins/todo/todo.plugin
index 1441842..bf5a742 100644
--- a/plugins/todo/todo.plugin
+++ b/plugins/todo/todo.plugin
@@ -1,8 +1,8 @@
 [Plugin]
-Module=todo_plugin
-Loader=python3
-Name=Todo Tracker
-Description=Extract todo items from source code
+Module=todo-plugin
+Name=To-Do Tracker
+Description=Find and present To-Do items from source code
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
+Copyright=Copyright © 2015-2017 Christian Hergert
 Builtin=true
+Depends=editor



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