[gnome-builder] todo: add a simple todo plugin



commit 551088f42fdae7d856e7ad5c0122b72ef310acf3
Author: Christian Hergert <chergert redhat com>
Date:   Fri Dec 25 05:28:59 2015 -0800

    todo: add a simple todo plugin
    
    This plugin had 3 purposes. 1) holiday hack. 2) be an example of how to
    quickly write a Builder plugin over a few hours. 3) remind us how much
    unfinished stuff we have.
    
    Anyway, enjoy your break from school/work/life. Because there's tons left
    waiting for you when you get back :)

 configure.ac                         |    2 +
 plugins/Makefile.am                  |    1 +
 plugins/todo/Makefile.am             |   15 +++
 plugins/todo/configure.ac            |   12 ++
 plugins/todo/todo.plugin             |    8 ++
 plugins/todo/todo_plugin/__init__.py |  221 ++++++++++++++++++++++++++++++++++
 6 files changed, 259 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 4e34c1b..137dd8f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -244,6 +244,7 @@ m4_include([plugins/python-pack/configure.ac])
 m4_include([plugins/support/configure.ac])
 m4_include([plugins/symbol-tree/configure.ac])
 m4_include([plugins/sysmon/configure.ac])
+m4_include([plugins/todo/configure.ac])
 m4_include([plugins/terminal/configure.ac])
 m4_include([plugins/vala-pack/configure.ac])
 m4_include([plugins/xml-pack/configure.ac])
@@ -497,6 +498,7 @@ echo "  Python Language Pack ................. : ${enable_python_pack_plugin}"
 echo "  Support .............................. : ${enable_support_plugin}"
 echo "  System Monitor ....................... : ${enable_sysmon_plugin}"
 echo "  Symbol Tree .......................... : ${enable_symbol_tree_plugin}"
+echo "  Todo ................................. : ${enable_todo_plugin}"
 echo "  Terminal ............................. : ${enable_terminal_plugin}"
 echo "  Vala Language Pack ................... : ${enable_vala_pack_plugin}"
 echo "  XML Language Pack .................... : ${enable_xml_pack_plugin}"
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index c2bc0fc..c0fc119 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -22,6 +22,7 @@ SUBDIRS = \
        symbol-tree \
        sysmon \
        terminal \
+       todo \
        vala-pack \
        xml-pack \
        $(NULL)
diff --git a/plugins/todo/Makefile.am b/plugins/todo/Makefile.am
new file mode 100644
index 0000000..192ee9b
--- /dev/null
+++ b/plugins/todo/Makefile.am
@@ -0,0 +1,15 @@
+if ENABLE_TODO_PLUGIN
+
+EXTRA_DIST = $(plugin_DATA)
+
+plugindir = $(libdir)/gnome-builder/plugins
+dist_plugin_DATA = todo.plugin
+
+moduledir = $(libdir)/gnome-builder/plugins/todo_plugin
+dist_module_DATA = todo_plugin/__init__.py
+
+endif
+
+GITIGNOREFILES = todo_plugin/__pycache__
+
+-include $(top_srcdir)/git.mk
diff --git a/plugins/todo/configure.ac b/plugins/todo/configure.ac
new file mode 100644
index 0000000..00697b8
--- /dev/null
+++ b/plugins/todo/configure.ac
@@ -0,0 +1,12 @@
+# --enable-todo-plugin=yes/no
+AC_ARG_ENABLE([todo-plugin],
+              [AS_HELP_STRING([--enable-todo-plugin=@<:@yes/no@:>@],
+                              [Build with support for searching files for todo items.])],
+              [enable_todo_plugin=$enableval],
+              [enable_todo_plugin=yes])
+
+# for if ENABLE_TODO_PLUGIN in Makefile.am
+AM_CONDITIONAL(ENABLE_TODO_PLUGIN, test x$enable_todo_plugin != xno)
+
+# Ensure our makefile is generated by autoconf
+AC_CONFIG_FILES([plugins/todo/Makefile])
diff --git a/plugins/todo/todo.plugin b/plugins/todo/todo.plugin
new file mode 100644
index 0000000..1441842
--- /dev/null
+++ b/plugins/todo/todo.plugin
@@ -0,0 +1,8 @@
+[Plugin]
+Module=todo_plugin
+Loader=python3
+Name=Todo Tracker
+Description=Extract todo items from source code
+Authors=Christian Hergert <christian hergert me>
+Copyright=Copyright © 2015 Christian Hergert
+Builtin=true
diff --git a/plugins/todo/todo_plugin/__init__.py b/plugins/todo/todo_plugin/__init__.py
new file mode 100644
index 0000000..f03659c
--- /dev/null
+++ b/plugins/todo/todo_plugin/__init__.py
@@ -0,0 +1,221 @@
+#!/usr/bin/env python3
+
+#
+# todo.py
+#
+# Copyright (C) 2015 Christian Hergert <chris dronelabs 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/>.
+#
+
+from gi.repository import Ide
+from gi.repository import Gio
+from gi.repository import GLib
+from gi.repository import GObject
+from gi.repository import Gtk
+
+from gettext import gettext as _
+import re
+import subprocess
+import threading
+
+LINE1 = re.compile('(.*):(\d+):(.*)')
+LINE2 = re.compile('(.*)-(\d+)-(.*)')
+KEYWORDS = ['FIXME:', 'XXX:', 'TODO:']
+
+class TodoWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
+    workbench = None
+    panel = None
+
+    def do_load(self, workbench):
+        self.workbench = workbench
+
+        # Watch the buffer manager for file changes (to update)
+        context = workbench.get_context()
+        bufmgr = context.get_buffer_manager()
+        bufmgr.connect('buffer-saved', self.on_buffer_saved)
+
+        # Get the working directory of the project
+        vcs = context.get_vcs()
+        workdir = vcs.get_working_directory()
+
+        # Create our panel to display results
+        self.panel = TodoPanel(workdir, visible=True)
+        editor = workbench.get_perspective_by_name('editor')
+        pane = editor.get_bottom_pane()
+        pane.add_page(self.panel, _("Todo"), None)
+
+        # Mine the directory in a background thread
+        self.mine(workdir)
+
+    def do_unload(self, workbench):
+        self.panel.destroy()
+        self.panel = None
+
+        self.workbench = None
+
+    def on_buffer_saved(self, bufmgr, buf):
+        # Get the underline GFile
+        file = buf.get_file().get_file()
+
+        # XXX: Clear existing items from this file
+
+        # Mine the file for todo items
+        self.mine(file)
+
+    def post(self, items):
+        context = self.workbench.get_context()
+        vcs = context.get_vcs()
+
+        for item in items:
+            if vcs.is_ignored(item.props.file):
+                continue
+            self.panel.add_item(item)
+
+    def mine(self, file):
+        """
+        Mine a file or directory.
+
+        We use a simple grep command to do the work for us rather than
+        trying to write anything too complex that would just approximate
+        the same thing anyway.
+        """
+        args = ['grep', '-A', '5', '-I', '-H', '-n', '-r']
+        for keyword in KEYWORDS:
+            args.append('-e')
+            args.append(keyword)
+        args.append(file.get_path())
+        p = subprocess.Popen(args, stdout=subprocess.PIPE)
+
+        def communicate(proc):
+            stdout, _ = proc.communicate()
+            lines = stdout.decode('utf-8').splitlines()
+            stdout = None
+
+            items = []
+            item = TodoItem()
+
+            for line in lines:
+                # Skip long lines, like from SVG files
+                if not line.strip() or len(line) > 1024:
+                    continue
+
+                if line.startswith('--'):
+                    if item.props.file:
+                        items.append(item)
+                    item = TodoItem()
+                    continue
+
+                # If there is no file, then we haven't reached the x:x: line
+                regex = LINE1 if not item.props.file else LINE2
+                try:
+                    (filename, line, message) = regex.match(line).groups()
+                except Exception as ex:
+                    continue
+
+                if not item.props.file:
+                    item.props.file = Gio.File.new_for_path(filename)
+                    item.props.line = int(line)
+
+                # XXX: not efficient use of roundtrips to/from pygobject
+                if item.props.message:
+                    item.props.message += '\n' + message
+                else:
+                    item.props.message = message
+
+            if item.props.file:
+                items.append(item)
+
+            GLib.timeout_add(0, lambda: self.post(items) and GLib.SOURCE_REMOVE)
+
+        threading.Thread(target=communicate, args=[p], name='todo-thread').start()
+
+class TodoItem(GObject.Object):
+    message = GObject.Property(type=str)
+    line = GObject.Property(type=int)
+    file = GObject.Property(type=Gio.File)
+
+    def __repr__(self):
+        return u'<TodoItem(%s:%d)>' % (self.props.file.get_path(), self.props.line)
+
+    @property
+    def shortdesc(self):
+        msg = self.props.message
+        if '\n' in msg:
+            return msg[:msg.index('\n')].strip()
+        return msg.strip()
+
+class TodoPanel(Gtk.Bin):
+    def __init__(self, basedir, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.basedir = basedir
+        self.model = Gtk.ListStore(TodoItem)
+
+        scroller = Gtk.ScrolledWindow(visible=True)
+        self.add(scroller)
+
+        treeview = Gtk.TreeView(visible=True, model=self.model, has_tooltip=True)
+        treeview.connect('query-tooltip', self.on_query_tooltip)
+        treeview.connect('row-activated', self.on_row_activated)
+        scroller.add(treeview)
+
+        column1 = Gtk.TreeViewColumn(title="File")
+        treeview.append_column(column1)
+
+        cell = Gtk.CellRendererText(xalign=0.0)
+        column1.pack_start(cell, True)
+        column1.set_cell_data_func(cell, self._file_data_func)
+
+        column2 = Gtk.TreeViewColumn(title="Message")
+        treeview.append_column(column2)
+
+        cell = Gtk.CellRendererText(xalign=0.0)
+        column2.pack_start(cell, True)
+        column2.set_cell_data_func(cell, self._message_data_func)
+
+    def _file_data_func(self, column, cell, model, iter, data):
+        item, = model.get(iter, 0)
+        relpath = self.basedir.get_relative_path(item.props.file)
+        if not relpath:
+            relpath = item.props.file.get_path()
+        cell.props.text = '%s:%u' % (relpath, item.props.line)
+
+    def _message_data_func(self, column, cell, model, iter, data):
+        item, = model.get(iter, 0)
+        cell.props.text = item.shortdesc
+
+    def add_item(self, item):
+        iter = self.model.append()
+        self.model.set_value(iter, 0, item)
+
+    def on_query_tooltip(self, treeview, x, y, keyboard, tooltip):
+        x, y = treeview.convert_widget_to_bin_window_coords(x, y)
+        try:
+            path, column, cell_x, cell_y = treeview.get_path_at_pos(x, y)
+            iter = self.model.get_iter(path)
+            item, = self.model.get(iter, 0)
+            tooltip.set_markup('<tt>' + GLib.markup_escape_text(item.props.message) + '</tt>')
+            return True
+        except:
+            return False
+
+    def on_row_activated(self, treeview, path, column):
+        iter = self.model.get_iter(path)
+        item, = self.model.get(iter, 0)
+        uri = Ide.Uri.new_from_file(item.props.file)
+        uri.set_fragment('L%u' % item.props.line)
+
+        workbench = self.get_ancestor(Ide.Workbench)
+        workbench.open_uri_async(uri, 'editor', None, None, None)


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