[gnome-builder] todo: add a simple todo plugin
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder] todo: add a simple todo plugin
- Date: Fri, 25 Dec 2015 13:40:09 +0000 (UTC)
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]