[gnome-builder/wip/libide] libide: add basic editorconfig file settings backend



commit 4da895ff008859303633bf945249e067e0a6f586
Author: Christian Hergert <christian hergert me>
Date:   Sat Feb 14 17:23:38 2015 -0800

    libide: add basic editorconfig file settings backend
    
    This still needs some work, but at least it gets us started. In particular,
    the editorconfig implementation is missing some features. We might go
    ahead and use the upstream editorconfig C library, but so far I'm not
    thrilled about it.

 libide/Makefile.am                                 |    5 +
 libide/editorconfig/editorconfig.c                 |  344 ++++++++++++++++++++
 libide/editorconfig/editorconfig.h                 |   20 ++
 .../editorconfig/ide-editorconfig-file-settings.c  |  179 ++++++++++
 .../editorconfig/ide-editorconfig-file-settings.h  |   36 ++
 libide/ide.c                                       |    6 +
 6 files changed, 590 insertions(+), 0 deletions(-)
---
diff --git a/libide/Makefile.am b/libide/Makefile.am
index 1f32e9e..15de308 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -26,6 +26,8 @@ libide_la_public_sources = \
        libide/directory/ide-directory-build-system.h \
        libide/directory/ide-directory-vcs.c \
        libide/directory/ide-directory-vcs.h \
+       libide/editorconfig/ide-editorconfig-file-settings.c \
+       libide/editorconfig/ide-editorconfig-file-settings.h \
        libide/git/ide-git-vcs.c \
        libide/git/ide-git-vcs.h \
        libide/ide-back-forward-item.c \
@@ -130,6 +132,8 @@ libide_la_public_sources = \
 
 libide_la_SOURCES = \
        $(libide_la_public_sources) \
+       libide/editorconfig/editorconfig.c \
+       libide/editorconfig/editorconfig.h \
        libide/gconstructor.h \
        libide/ide-async-helper.c \
        libide/ide-async-helper.h \
@@ -144,6 +148,7 @@ libide_la_CFLAGS = \
        -I$(top_srcdir)/libide/c \
        -I$(top_srcdir)/libide/clang \
        -I$(top_srcdir)/libide/directory \
+       -I$(top_srcdir)/libide/editorconfig \
        -I$(top_srcdir)/libide/git \
        -I$(top_srcdir)/libide/local \
        -I$(top_srcdir)/libide/tasks \
diff --git a/libide/editorconfig/editorconfig.c b/libide/editorconfig/editorconfig.c
new file mode 100644
index 0000000..4e298b2
--- /dev/null
+++ b/libide/editorconfig/editorconfig.c
@@ -0,0 +1,344 @@
+/*
+ * Authors: Christian Hergert <christian hergert me>
+ *
+ * The author or authors of this code dedicate any and all copyright interest
+ * in this code to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and successors. We
+ * intend this dedication to be an overt act of relinquishment in perpetuity of
+ * all present and future rights to this code under copyright law.
+ */
+
+#include <fnmatch.h>
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include "editorconfig.h"
+
+static gboolean
+glob_match (const gchar  *pattern,
+            const gchar  *string,
+            GError      **error)
+{
+  int flags;
+  int ret;
+
+  if (g_str_equal (pattern, "__global__"))
+    return TRUE;
+
+  flags = FNM_PATHNAME | FNM_PERIOD | FNM_CASEFOLD;
+
+  ret = fnmatch (pattern, string, flags);
+
+  switch (ret)
+    {
+    case 0:
+      return TRUE;
+
+    case FNM_NOMATCH:
+      return FALSE;
+
+    default:
+      return FALSE;
+    }
+}
+
+static void
+vfree (gpointer data)
+{
+  GValue *value = data;
+
+  if (value)
+    {
+      g_value_unset (value);
+      g_free (value);
+    }
+}
+
+static gboolean
+parse_key (GKeyFile     *key_file,
+           const gchar  *group,
+           const gchar  *key,
+           GHashTable   *hashtable,
+           GError      **error)
+{
+  gchar *lower;
+  GValue *value;
+  gboolean ret = FALSE;
+
+  g_assert (key_file);
+  g_assert (group);
+  g_assert (key);
+  g_assert (hashtable);
+
+  lower = g_utf8_strdown (key, -1);
+  value = g_new0 (GValue, 1);
+
+  if (g_str_equal (key, "root"))
+    {
+      if (g_key_file_get_boolean (key_file, group, key, NULL))
+        g_hash_table_remove_all (hashtable);
+      ret = TRUE;
+      goto cleanup;
+    }
+  else if (g_str_equal (key, "indent_size") || g_str_equal (key, "tab_width"))
+    {
+      GError *local_error = NULL;
+      gint v;
+
+      v = g_key_file_get_integer (key_file, group, key, &local_error);
+
+      if (local_error)
+        {
+          g_propagate_error (error, local_error);
+          goto cleanup;
+        }
+
+      g_value_init (value, G_TYPE_UINT);
+      g_value_set_uint (value, MAX (0, v));
+    }
+  else if (g_str_equal (key, "trim_trailing_whitespace") || g_str_equal (key, "insert_final_newline"))
+    {
+      GError *local_error = NULL;
+      gboolean v;
+
+      v = g_key_file_get_boolean (key_file, group, key, &local_error);
+
+      if (local_error)
+        {
+          g_propagate_error (error, local_error);
+          goto cleanup;
+        }
+
+      g_value_init (value, G_TYPE_BOOLEAN);
+      g_value_set_boolean (value, v);
+    }
+  else
+    {
+      gchar *str;
+
+      str = g_key_file_get_string (key_file, group, key, NULL);
+      g_value_init (value, G_TYPE_STRING);
+      g_value_take_string (value, str);
+    }
+
+  g_assert (G_VALUE_TYPE (value) != G_TYPE_NONE);
+  g_hash_table_replace (hashtable, g_strdup (key), value);
+  value = NULL;
+
+  ret = TRUE;
+
+cleanup:
+  g_free (value);
+  g_free (lower);
+
+  return ret;
+}
+
+static gboolean
+parse_group (GKeyFile     *key_file,
+             const gchar  *group,
+             GHashTable   *hashtable,
+             const gchar  *relpath,
+             GError      **error)
+{
+  gchar **keys = NULL;
+  gsize i;
+  gboolean ret = FALSE;
+
+  g_assert (key_file);
+  g_assert (hashtable);
+  g_assert (relpath);
+
+  if (!(keys = g_key_file_get_keys (key_file, group, NULL, error)))
+    goto cleanup;
+
+  for (i = 0; keys [i]; i++)
+    {
+      if (!parse_key (key_file, group, keys [i], hashtable, error))
+        goto cleanup;
+    }
+
+  ret = TRUE;
+
+cleanup:
+  g_clear_pointer (&keys, g_strfreev);
+
+  return ret;
+}
+
+static gboolean
+parse_file (GFile         *doteditorconfig,
+            GCancellable  *cancellable,
+            GHashTable    *hashtable,
+            GFile         *target,
+            GError       **error)
+{
+  GKeyFile *key_file = NULL;
+  GString *mutated = NULL;
+  gchar *contents = NULL;
+  GFile *parent = NULL;
+  gchar *relpath = NULL;
+  gchar **groups = NULL;
+  gsize len;
+  gsize i;
+  gboolean ret = FALSE;
+
+  g_assert (G_IS_FILE (doteditorconfig));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (hashtable);
+  g_assert (G_IS_FILE (target));
+
+  parent = g_file_get_parent (doteditorconfig);
+  g_assert (G_IS_FILE (parent));
+
+  relpath = g_file_get_relative_path (parent, target);
+  g_assert (relpath);
+
+  if (!g_file_load_contents (doteditorconfig, cancellable, &contents, &len, NULL, error))
+    goto cleanup;
+
+  if (!g_utf8_validate (contents, len, NULL))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   _(".editorconfig did not contain valid UTF-8"));
+      goto cleanup;
+    }
+
+  /* .editorconfig can have settings before a keyfile group */
+  mutated = g_string_new ("[__global__]\n");
+  g_string_append (mutated, contents);
+
+  key_file = g_key_file_new ();
+
+  if (!g_key_file_load_from_data (key_file, mutated->str, mutated->len, 0, error))
+    goto cleanup;
+
+  groups = g_key_file_get_groups (key_file, NULL);
+
+  for (i = 0; groups [i]; i++)
+    {
+      gboolean matches;
+      GError *local_error = NULL;
+
+      matches = glob_match (groups [i], relpath, &local_error);
+
+      if (local_error)
+        {
+          g_propagate_error (error, local_error);
+          goto cleanup;
+        }
+
+      if (matches)
+        {
+          if (!parse_group (key_file, groups [i], hashtable, relpath, error))
+            goto cleanup;
+        }
+    }
+
+  ret = TRUE;
+
+cleanup:
+  if (mutated)
+    g_string_free (mutated, TRUE);
+  g_clear_pointer (&key_file, g_key_file_free);
+  g_clear_pointer (&relpath, g_free);
+  g_clear_pointer (&contents, g_free);
+  g_clear_pointer (&groups, g_strfreev);
+  g_clear_object (&parent);
+
+  return ret;
+}
+
+/**
+ * editorconfig_read:
+ * @file: A #GFile containing the file to apply the settings for.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @error: (out) (allow-none): A location for a #GError, or %NULL.
+ *
+ * This function will read the .editorconfig rules that match @file starting
+ * from it's parent directory and working it's way up to the root of the of
+ * the project tree.
+ *
+ * Returns: (transfer container) (element-type gchar* GValue*): A #GHashTable
+ *   containing the key/value pairs that should be applied to @file.
+ */
+GHashTable *
+editorconfig_read (GFile         *file,
+                   GCancellable  *cancellable,
+                   GError       **error)
+{
+  GHashTable *hashtable;
+  GQueue *queue;
+  GFile *iter;
+
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);
+  g_return_val_if_fail (!error || !*error, NULL);
+
+  /*
+   * The following is a simple algorithm for applying the editorconfig files
+   * in reverse-order so that the closer to the target file .editorconfig is,
+   * the higher it's precedence.
+   *
+   * We work our way down starting from the sibling .editorconfig for the file.
+   * If the file exists, we push it onto the queue's head.
+   *
+   * Once we know about all of the potential files working our way to the root
+   * of the filesystem, we can pop items off the queues head and apply them
+   * to the hashtable in order.
+   *
+   * The result is a hashtable containing string keys and GValue* values.
+   *
+   * Note that if we discover a "root = true" key along the way, we can simply
+   * clear the hashtable to get the same affect as if we were to read each
+   * file as we dove down. The reason we don't do that is that we then have to
+   * hold on to all of the files in memory at once, instead of potentially
+   * doing a little extra I/O starting from the root. I think the tradeoff
+   * results in a bit cleaner code, so I'm going with that.
+   */
+
+  queue = g_queue_new ();
+  iter = g_object_ref (file);
+ 
+  do
+    {
+      GFile *parent;
+      GFile *doteditorconfig;
+
+      parent = g_file_get_parent (iter);
+      doteditorconfig = g_file_get_child (parent, ".editorconfig");
+
+      if (g_file_query_exists (doteditorconfig, cancellable))
+        g_queue_push_head (queue, g_object_ref (doteditorconfig));
+
+      g_clear_object (&doteditorconfig);
+      g_clear_object (&iter);
+
+      iter = parent;
+    }
+  while (g_file_has_parent (iter, NULL));
+
+  g_clear_object (&iter);
+
+  hashtable = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, vfree);
+
+  while ((iter = g_queue_pop_head (queue)))
+    {
+      if (!parse_file (iter, cancellable, hashtable, file, error))
+        {
+          g_object_unref (iter);
+          goto cleanup;
+        }
+
+      g_object_unref (iter);
+    }
+
+cleanup:
+  while ((iter = g_queue_pop_head (queue)))
+    g_object_unref (iter);
+  g_queue_free (queue);
+
+  return hashtable;
+}
diff --git a/libide/editorconfig/editorconfig.h b/libide/editorconfig/editorconfig.h
new file mode 100644
index 0000000..d3bda86
--- /dev/null
+++ b/libide/editorconfig/editorconfig.h
@@ -0,0 +1,20 @@
+/*
+ * Authors: Christian Hergert <christian hergert me>
+ *
+ * The author or authors of this code dedicate any and all copyright interest
+ * in this code to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and successors. We
+ * intend this dedication to be an overt act of relinquishment in perpetuity of
+ * all present and future rights to this code under copyright law.
+ */
+
+#ifndef EDITORCONFIG_H
+#define EDITORCONFIG_H
+
+#include <gio/gio.h>
+
+GHashTable *editorconfig_read (GFile         *file,
+                               GCancellable  *cancellable,
+                               GError       **error);
+
+#endif /* EDITORCONFIG_H */
diff --git a/libide/editorconfig/ide-editorconfig-file-settings.c 
b/libide/editorconfig/ide-editorconfig-file-settings.c
new file mode 100644
index 0000000..aaf998a
--- /dev/null
+++ b/libide/editorconfig/ide-editorconfig-file-settings.c
@@ -0,0 +1,179 @@
+/* ide-editorconfig-file-settings.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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 <editorconfig.h>
+#include <glib/gi18n.h>
+
+#include "ide-editorconfig-file-settings.h"
+#include "ide-file.h"
+
+struct _IdeEditorconfigFileSettings
+{
+  IdeFileSettings parent_instance;
+};
+
+static void async_initable_iface_init (GAsyncInitableIface *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeEditorconfigFileSettings,
+                        ide_editorconfig_file_settings,
+                        IDE_TYPE_FILE_SETTINGS,
+                        0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
+                                               async_initable_iface_init))
+
+static void
+ide_editorconfig_file_settings_class_init (IdeEditorconfigFileSettingsClass *klass)
+{
+}
+
+static void
+ide_editorconfig_file_settings_init (IdeEditorconfigFileSettings *self)
+{
+}
+
+static void
+ide_editorconfig_file_settings_init_worker (GTask        *task,
+                                            gpointer      source_object,
+                                            gpointer      task_data,
+                                            GCancellable *cancellable)
+{
+  GFile *file = task_data;
+  GHashTableIter iter;
+  GHashTable *ht;
+  gpointer k, v;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (IDE_IS_EDITORCONFIG_FILE_SETTINGS (source_object));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ht = editorconfig_read (file, cancellable, &error);
+
+  if (!ht)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
+  g_hash_table_iter_init (&iter, ht);
+
+  while (g_hash_table_iter_next (&iter, &k, &v))
+    {
+      const gchar *prop_name;
+      const gchar *key = k;
+      const GValue *value = v;
+
+      if (g_str_equal (key, "indent_size"))
+        g_object_set_property (source_object, "indent_width", value);
+      else if (g_str_equal (key, "tab_width") ||
+               g_str_equal (key, "trim_trailing_whitespace") ||
+               g_str_equal (key, "insert_final_newline"))
+        g_object_set_property (source_object, key, value);
+      else if (g_str_equal (key, "charset"))
+        g_object_set_property (source_object, "encoding", value);
+      else if (g_str_equal (key, "end_of_line"))
+        {
+          GtkSourceNewlineType newline_type = GTK_SOURCE_NEWLINE_TYPE_LF;
+          const gchar *str;
+
+          str = g_value_get_string (value);
+          if (g_strcmp0 (str, "cr") == 0)
+            newline_type = GTK_SOURCE_NEWLINE_TYPE_CR;
+          else if (g_strcmp0 (str, "crlf") == 0)
+            newline_type = GTK_SOURCE_NEWLINE_TYPE_CR_LF;
+
+          ide_file_settings_set_newline_type (source_object, newline_type);
+        }
+      else if (g_str_equal (key, "indent_style"))
+        {
+          IdeIndentStyle indent_style = IDE_INDENT_STYLE_SPACES;
+          const gchar *str;
+
+          str = g_value_get_string (value);
+
+          if (g_strcmp0 (str, "tab"))
+            indent_style = IDE_INDENT_STYLE_TABS;
+
+          ide_file_settings_set_indent_style (source_object, indent_style);
+        }
+    }
+
+  if (g_hash_table_size (ht) == 0)
+    g_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_FOUND,
+                             _("No editorconfig options were found."));
+  else
+    g_task_return_boolean (task, TRUE);
+
+  g_hash_table_unref (ht);
+}
+
+static void
+ide_editorconfig_file_settings_init_async (GAsyncInitable      *initable,
+                                           gint                 io_priority,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  IdeEditorconfigFileSettings *self = (IdeEditorconfigFileSettings *)initable;
+  g_autoptr(GTask) task = NULL;
+  IdeFile *file;
+  GFile *gfile = NULL;
+
+  g_return_if_fail (IDE_IS_EDITORCONFIG_FILE_SETTINGS (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  file = ide_file_settings_get_file (IDE_FILE_SETTINGS (self));
+  if (file)
+    gfile = ide_file_get_file (file);
+
+  if (!gfile)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_FOUND,
+                               _("No file was provided."));
+      return;
+    }
+
+  g_task_set_task_data (task, g_object_ref (gfile), g_object_unref);
+  g_task_run_in_thread (task, ide_editorconfig_file_settings_init_worker);
+}
+
+static gboolean
+ide_editorconfig_file_settings_init_finish (GAsyncInitable  *initable,
+                                            GAsyncResult    *result,
+                                            GError         **error)
+{
+  GTask *task = (GTask *)result;
+
+  g_return_val_if_fail (G_IS_TASK (task), FALSE);
+
+  return g_task_propagate_boolean (task, error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = ide_editorconfig_file_settings_init_async;
+  iface->init_finish = ide_editorconfig_file_settings_init_finish;
+}
diff --git a/libide/editorconfig/ide-editorconfig-file-settings.h 
b/libide/editorconfig/ide-editorconfig-file-settings.h
new file mode 100644
index 0000000..1158bd7
--- /dev/null
+++ b/libide/editorconfig/ide-editorconfig-file-settings.h
@@ -0,0 +1,36 @@
+/* ide-editorconfig-file-settings.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef IDE_EDITORCONFIG_FILE_SETTINGS_H
+#define IDE_EDITORCONFIG_FILE_SETTINGS_H
+
+#include "ide-file-settings.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_EDITORCONFIG_FILE_SETTINGS \
+  (ide_editorconfig_file_settings_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeEditorconfigFileSettings,
+                      ide_editorconfig_file_settings,
+                      IDE, EDITORCONFIG_FILE_SETTINGS,
+                      IdeFileSettings)
+
+G_END_DECLS
+
+#endif /* IDE_EDITORCONFIG_FILE_SETTINGS_H */
diff --git a/libide/ide.c b/libide/ide.c
index adb898b..b59a0fe 100644
--- a/libide/ide.c
+++ b/libide/ide.c
@@ -27,6 +27,7 @@
 #include "ide-clang-service.h"
 #include "ide-directory-build-system.h"
 #include "ide-directory-vcs.h"
+#include "ide-editorconfig-file-settings.h"
 #include "ide-file-settings.h"
 #include "ide-git-vcs.h"
 
@@ -75,6 +76,11 @@ ide_init_ctor (void)
                                   IDE_BUILD_SYSTEM_EXTENSION_POINT".directory",
                                   -200);
 
+  g_io_extension_point_implement (IDE_FILE_SETTINGS_EXTENSION_POINT,
+                                  IDE_TYPE_EDITORCONFIG_FILE_SETTINGS,
+                                  IDE_FILE_SETTINGS_EXTENSION_POINT".editorconfig",
+                                  0);
+
   g_io_extension_point_implement (IDE_LANGUAGE_EXTENSION_POINT,
                                   IDE_TYPE_C_LANGUAGE,
                                   IDE_LANGUAGE_EXTENSION_POINT".c",


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