[gnome-todo] todo-txt: add subtask implementation



commit 5a1b86cc71618d140f671069c27714c15da1e39e
Author: Rohit Kaushik <kaushikrohit325 gmail com>
Date:   Thu Jun 21 01:54:41 2018 +0530

    todo-txt: add subtask implementation
    
    This patch allows indenting task line as subtask using 4 initial
    spaces. First all the tasks are parsed and indention stored,
    after which the subtask heirarchy is resolved using indent
    level.
    
    Some heuristics:
        1) The difference between current line indent and previous
        line indent cannot exceed 1, if it does, the difference is
        taken 1.
    
        2) Tabs should not be used for indenting subtask. If tabs are
        present the parsing is stopped and a warning generated.

 plugins/todo-txt/gtd-provider-todo-txt.c | 118 +++++++++++++++++++++++++++++--
 plugins/todo-txt/gtd-todo-txt-parser.c   |  53 +++++++++++++-
 plugins/todo-txt/gtd-todo-txt-parser.h   |   6 +-
 3 files changed, 168 insertions(+), 9 deletions(-)
---
diff --git a/plugins/todo-txt/gtd-provider-todo-txt.c b/plugins/todo-txt/gtd-provider-todo-txt.c
index e29dd07..7f4821f 100644
--- a/plugins/todo-txt/gtd-provider-todo-txt.c
+++ b/plugins/todo-txt/gtd-provider-todo-txt.c
@@ -92,6 +92,7 @@ print_task (GString *output,
   GDateTime *creation_dt;
   const gchar *description;
   gint priority;
+  gint depth;
   gboolean is_complete;
 
   is_complete = gtd_task_get_complete (task);
@@ -101,6 +102,11 @@ print_task (GString *output,
   description = gtd_task_get_description (task);
   creation_dt = gtd_task_get_creation_date (task);
   completion_dt = gtd_task_get_completion_date (task);
+  depth = gtd_task_get_depth (task);
+
+  /* First add spaces for indentation */
+  if (depth > 0)
+    g_string_append_printf (output, "%*c", depth * INDENT_LEN, ' ');
 
   if (is_complete)
     g_string_append (output, "x ");
@@ -191,7 +197,10 @@ update_source (GtdProviderTodoTxt *self)
       list = g_ptr_array_index (self->cache, i);
       tasks = gtd_task_list_get_tasks (list);
 
-      /* And now each task */
+      /* First sort the tasks */
+      tasks = g_list_sort (tasks, (GCompareFunc) gtd_task_compare);
+
+      /* And now save each task */
       for (l = tasks; l; l = l->next)
         print_task (contents, l->data);
     }
@@ -285,14 +294,20 @@ parse_list_colors_line (GtdProviderTodoTxt *self,
 }
 
 static void
-parse_task (GtdProviderTodoTxt *self,
-            const gchar        *line)
+parse_task (GtdProviderTodoTxt  *self,
+            const gchar         *line,
+            GHashTable          *list_to_tasks,
+            GError             **error)
 {
   g_autofree gchar *list_name = NULL;
   GtdTaskList *list;
+  GPtrArray *tasks;
   GtdTask *task;
 
-  task = gtd_todo_txt_parser_parse_task (GTD_PROVIDER (self), line, &list_name);
+  task = gtd_todo_txt_parser_parse_task (GTD_PROVIDER (self), line, &list_name, error);
+
+  if (!task)
+    GTD_RETURN ();
 
   /*
    * Create the list if it doesn't exist yet; this might happen with todo.txt files
@@ -317,6 +332,13 @@ parse_task (GtdProviderTodoTxt *self,
       list = g_hash_table_lookup (self->lists, list_name);
     }
 
+  /* Add to the temporary GPtrArray that will be consumed below */
+  if (!g_hash_table_contains (list_to_tasks, list))
+    g_hash_table_insert (list_to_tasks, list, g_ptr_array_new ());
+
+  tasks = g_hash_table_lookup (list_to_tasks, list);
+  g_ptr_array_add (tasks, task);
+
   gtd_task_set_list (task, list);
   gtd_task_list_save_task (list, task);
 
@@ -349,7 +371,11 @@ remove_empty_lines (GStrv lines)
     {
       gchar *line;
 
-      line = g_strstrip (lines[i]);
+      /*
+       * We can only remove the trailing space here since leading
+       * whitespace determine the indentation and subtasks
+       */
+      line = g_strchomp (lines[i]);
 
       if (!line || line[0] == '\n' || line[0] == '\0')
         continue;
@@ -360,9 +386,68 @@ remove_empty_lines (GStrv lines)
   return g_steal_pointer (&valid_lines);
 }
 
+static void
+resolve_subtasks (GtdProviderTodoTxt *self,
+                  GHashTable         *list_to_tasks)
+{
+  GtdTaskList *list;
+  GPtrArray *tasks;
+  GHashTableIter iter;
+  GQueue tasks_stack;
+
+  g_queue_init (&tasks_stack);
+  g_hash_table_iter_init (&iter, list_to_tasks);
+
+  while (g_hash_table_iter_next (&iter, (gpointer) &list, (gpointer) &tasks))
+    {
+      gint64 previous_indent;
+      guint i;
+
+      previous_indent = 0;
+
+      GTD_TRACE_MSG ("Setting up tasklist '%s'", gtd_task_list_get_name (list));
+
+      for (i = 0; tasks && i < tasks->len; i++)
+        {
+          GtdTask *parent_task;
+          GtdTask *task;
+          guint indent;
+          guint j;
+
+          task = g_ptr_array_index (tasks, i);
+          parent_task = NULL;
+          indent = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "indent"));
+
+          GTD_TRACE_MSG ("  Adding task '%s' (%s)",
+                         gtd_task_get_title (task),
+                         gtd_object_get_uid (GTD_OBJECT (task)));
+
+          /* If the indent changed, remove from the difference in level from stack */
+          for (j = 0; j <= previous_indent - indent; j++)
+            g_queue_pop_head (&tasks_stack);
+
+          parent_task = g_queue_peek_head (&tasks_stack);
+
+          if (parent_task)
+            gtd_task_add_subtask (parent_task, task);
+
+          gtd_task_set_list (task, list);
+          gtd_task_list_save_task (list, task);
+
+          g_queue_push_head (&tasks_stack, task);
+
+          previous_indent = indent;
+        }
+
+      /* Clear the queue since we're changing projects */
+      g_queue_clear (&tasks_stack);
+    }
+}
+
 static void
 reload_tasks (GtdProviderTodoTxt *self)
 {
+  g_autoptr (GHashTable) list_to_tasks = NULL;
   g_autofree gchar *input_path = NULL;
   g_autofree gchar *file_contents = NULL;
   g_autoptr (GPtrArray) valid_lines = NULL;
@@ -401,12 +486,15 @@ reload_tasks (GtdProviderTodoTxt *self)
     {
       g_autoptr (GError) line_error = NULL;
       GtdTodoTxtLineType line_type;
-      const gchar *line;
+      gchar *line;
       guint line_number;
 
       line_number = n_lines - vtable_len + i;
       line = g_ptr_array_index (valid_lines, line_number);
 
+      /* Since leading whitespace doesn't matter here, remove them */
+      line = g_strstrip (line);
+
       line_type = gtd_todo_txt_parser_get_line_type (line, &line_error);
 
       if (line_error)
@@ -425,6 +513,12 @@ reload_tasks (GtdProviderTodoTxt *self)
         }
     }
 
+  /* First, create all the tasks and store them temporarily in a GPtrArray */
+  list_to_tasks = g_hash_table_new_full (g_direct_hash,
+                                         g_direct_equal,
+                                         NULL,
+                                         (GDestroyNotify) g_ptr_array_unref);
+
   /* Then regular task lines */
   for (i = 0; i < n_lines - vtable_len; i++)
     {
@@ -454,7 +548,7 @@ reload_tasks (GtdProviderTodoTxt *self)
           break;
 
         case GTD_TODO_TXT_LINE_TYPE_TASK:
-          parse_task (self, line);
+          parse_task (self, line, list_to_tasks, &line_error);
           break;
 
         case GTD_TODO_TXT_LINE_TYPE_LIST_COLORS:
@@ -466,8 +560,18 @@ reload_tasks (GtdProviderTodoTxt *self)
         default:
           break;
         }
+
+      if (line_error)
+        g_warning ("Error parsing line %d: %s", i + 1, line_error->message);
     }
 
+  /*
+   * Now that all the tasks are created and properly stored in a GPtrArray,
+   * we have to go through each GPtrArray, sort it, and figure out the parent
+   * and children relationship between the tasks.
+   */
+  resolve_subtasks (self, list_to_tasks);
+
   GTD_EXIT;
 }
 
diff --git a/plugins/todo-txt/gtd-todo-txt-parser.c b/plugins/todo-txt/gtd-todo-txt-parser.c
index 9eb7470..04dbe19 100644
--- a/plugins/todo-txt/gtd-todo-txt-parser.c
+++ b/plugins/todo-txt/gtd-todo-txt-parser.c
@@ -220,10 +220,55 @@ tokenize_line (const gchar *line,
   return tokens;
 }
 
+/**
+ * get_line_indentation:
+ * @line: the tasklist line to be parsed
+ *
+ * Parses indentation level of a task line
+ *
+ * Returns: indentation level or -1 if error
+ */
+static gint
+get_line_indentation (const gchar  *line,
+                      GError      **error)
+{
+  guint indent_level = 0;
+  guint leading_space = 0;
+  guint i;
+
+  GTD_ENTRY;
+
+  for (i = 0; line[i] != '\0'; i++)
+    {
+      if (line[i] == ' ')
+        {
+          leading_space += 1;
+        }
+      else if (line[i] == '\t')
+        {
+          g_set_error (error,
+                       GTD_TODO_TXT_PARSER_ERROR,
+                       GTD_TODO_TXT_PARSER_INVALID_INDENT,
+                       "Invalid tabs found in task indentation");
+
+          GTD_RETURN (-1);
+        }
+      else
+        {
+          break;
+        }
+    }
+
+  indent_level = leading_space / INDENT_LEN;
+
+  GTD_RETURN (indent_level);
+}
+
 GtdTask*
 gtd_todo_txt_parser_parse_task (GtdProvider  *provider,
                                 const gchar  *line,
-                                gchar       **out_list_name)
+                                gchar       **out_list_name,
+                                GError      **error)
 {
   g_autoptr (GString) parent_task_name = NULL;
   g_autoptr (GString) list_name = NULL;
@@ -234,6 +279,7 @@ gtd_todo_txt_parser_parse_task (GtdProvider  *provider,
   g_auto (GStrv) tokens = NULL;
   GDateTime *dt;
   Token token_id;
+  gint indent;
   guint i;
 
   dt = NULL;
@@ -243,6 +289,10 @@ gtd_todo_txt_parser_parse_task (GtdProvider  *provider,
   parent_task_name = g_string_new (NULL);
   state.last_token = TOKEN_START;
   state.in_description = FALSE;
+  indent = get_line_indentation (line, error);
+
+  if (indent == -1)
+    return NULL;
 
   task = GTD_TASK (gtd_provider_todo_txt_generate_task (GTD_PROVIDER_TODO_TXT (provider)));
   tokens = tokenize_line (line, " ");
@@ -312,6 +362,7 @@ gtd_todo_txt_parser_parse_task (GtdProvider  *provider,
 
   gtd_task_set_title (task, title->str);
   gtd_task_set_description (task, note->str);
+  g_object_set_data (G_OBJECT (task), "indent", GINT_TO_POINTER (indent));
 
   if (out_list_name)
     *out_list_name = g_strdup (list_name->str + 1);
diff --git a/plugins/todo-txt/gtd-todo-txt-parser.h b/plugins/todo-txt/gtd-todo-txt-parser.h
index b94fd07..b49721c 100644
--- a/plugins/todo-txt/gtd-todo-txt-parser.h
+++ b/plugins/todo-txt/gtd-todo-txt-parser.h
@@ -19,6 +19,8 @@
 #ifndef GTD_TODO_TXT_PARSE_H
 #define GTD_TODO_TXT_PARSE_H
 
+#define INDENT_LEN 4
+
 #include "gnome-todo.h"
 #include "gtd-task-todo-txt.h"
 
@@ -30,6 +32,7 @@ typedef enum
 {
   GTD_TODO_TXT_PARSER_INVALID_DUE_DATE,
   GTD_TODO_TXT_PARSER_INVALID_COLOR_HEX,
+  GTD_TODO_TXT_PARSER_INVALID_INDENT,
   GTD_TODO_TXT_PARSER_INVALID_LINE,
   GTD_TODO_TXT_PARSER_UNSUPPORTED_TOKEN,
   GTD_TODO_TXT_PARSER_WRONG_LINE_TYPE,
@@ -56,7 +59,8 @@ GPtrArray*           gtd_todo_txt_parser_parse_task_lists        (GtdProvider
 
 GtdTask*             gtd_todo_txt_parser_parse_task              (GtdProvider       *provider,
                                                                   const gchar       *line,
-                                                                  gchar            **out_list_name);
+                                                                  gchar            **out_list_name,
+                                                                  GError           **error);
 
 gboolean             gtd_todo_txt_parser_parse_task_list_color   (GHashTable        *name_to_tasklist,
                                                                   const gchar       *line,


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