/* ide-context.c * * Copyright 2014-2019 Christian Hergert * * 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-context" #include "config.h" #include #include #include "ide-context.h" #include "ide-context-private.h" #include "ide-context-addin.h" #include "ide-macros.h" #include "ide-notifications.h" /** * SECTION:ide-context * @title: IdeContext * @short_description: the root object for a project * * The #IdeContext object is the root object for a project. Everything * in a project is contained by this object. * * Since: 3.32 */ struct _IdeContext { IdeObject parent_instance; PeasExtensionSet *addins; gchar *project_id; gchar *title; GFile *workdir; guint project_loaded : 1; }; enum { PROP_0, PROP_PROJECT_ID, PROP_TITLE, PROP_WORKDIR, N_PROPS }; enum { LOG, N_SIGNALS }; G_DEFINE_FINAL_TYPE (IdeContext, ide_context, IDE_TYPE_OBJECT) static GParamSpec *properties [N_PROPS]; static guint signals [N_SIGNALS]; static void ide_context_addin_load_project_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeContextAddin *addin = (IdeContextAddin *)object; g_autoptr(IdeContext) self = user_data; g_autoptr(GError) error = NULL; g_assert (IDE_IS_CONTEXT_ADDIN (addin)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_CONTEXT (self)); if (ide_context_addin_load_project_finish (addin, result, &error)) ide_context_addin_project_loaded (addin, self); else g_warning ("%s context addin failed to load project: %s", G_OBJECT_TYPE_NAME (addin), error->message); } static void ide_context_addin_added_cb (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeContextAddin *addin = (IdeContextAddin *)exten; IdeContext *self = user_data; g_autoptr(GCancellable) cancellable = NULL; g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_CONTEXT_ADDIN (addin)); /* Ignore any request during shutdown */ cancellable = ide_object_ref_cancellable (IDE_OBJECT (self)); if (g_cancellable_is_cancelled (cancellable)) return; ide_context_addin_load (addin, self); if (self->project_loaded) ide_context_addin_load_project_async (addin, self, cancellable, ide_context_addin_load_project_cb, g_object_ref (self)); } static void ide_context_addin_removed_cb (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeContextAddin *addin = (IdeContextAddin *)exten; IdeContext *self = user_data; g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_CONTEXT_ADDIN (addin)); ide_context_addin_unload (addin, self); } static void ide_context_real_log (IdeContext *self, GLogLevelFlags level, const gchar *domain, const gchar *message) { g_log (domain, level, "%s", message); } static gchar * ide_context_repr (IdeObject *object) { IdeContext *self = IDE_CONTEXT (object); return g_strdup_printf ("%s workdir=\"%s\" has_project=%d", G_OBJECT_TYPE_NAME (self), g_file_peek_path (self->workdir), self->project_loaded); } static void ide_context_constructed (GObject *object) { IdeContext *self = (IdeContext *)object; g_assert (IDE_IS_OBJECT (object)); self->addins = peas_extension_set_new (peas_engine_get_default (), IDE_TYPE_CONTEXT_ADDIN, NULL); g_signal_connect (self->addins, "extension-added", G_CALLBACK (ide_context_addin_added_cb), self); g_signal_connect (self->addins, "extension-removed", G_CALLBACK (ide_context_addin_removed_cb), self); peas_extension_set_foreach (self->addins, ide_context_addin_added_cb, self); G_OBJECT_CLASS (ide_context_parent_class)->constructed (object); } static void ide_context_destroy (IdeObject *object) { IdeContext *self = (IdeContext *)object; g_assert (IDE_IS_OBJECT (object)); g_clear_object (&self->addins); IDE_OBJECT_CLASS (ide_context_parent_class)->destroy (object); } static void ide_context_finalize (GObject *object) { IdeContext *self = (IdeContext *)object; g_clear_object (&self->workdir); g_clear_pointer (&self->project_id, g_free); g_clear_pointer (&self->title, g_free); G_OBJECT_CLASS (ide_context_parent_class)->finalize (object); } static void ide_context_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { IdeContext *self = IDE_CONTEXT (object); switch (prop_id) { case PROP_PROJECT_ID: g_value_take_string (value, ide_context_dup_project_id (self)); break; case PROP_TITLE: g_value_take_string (value, ide_context_dup_title (self)); break; case PROP_WORKDIR: g_value_take_object (value, ide_context_ref_workdir (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_context_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { IdeContext *self = IDE_CONTEXT (object); switch (prop_id) { case PROP_PROJECT_ID: ide_context_set_project_id (self, g_value_get_string (value)); break; case PROP_TITLE: ide_context_set_title (self, g_value_get_string (value)); break; case PROP_WORKDIR: ide_context_set_workdir (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_context_class_init (IdeContextClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass); object_class->constructed = ide_context_constructed; object_class->finalize = ide_context_finalize; object_class->get_property = ide_context_get_property; object_class->set_property = ide_context_set_property; i_object_class->destroy = ide_context_destroy; i_object_class->repr = ide_context_repr; /** * IdeContext:project-id: * * The "project-id" property is the identifier to use when creating * files and folders for this project. It has a mutated form of either * the directory or some other discoverable trait of the project. * * It has also been modified to remove spaces and other unsafe * characters for file-systems. * * This may change during runtime, but usually only once when the * project has been initialize loaded. * * Before any project has loaded, this is "empty" to allow flexibility * for non-project use. * * Since: 3.32 */ properties [PROP_PROJECT_ID] = g_param_spec_string ("project-id", "Project Id", "The project identifier used when creating files and folders", "empty", (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); /** * IdeContext:title: * * The "title" property is a descriptive name for the project. * * Since: 3.32 */ properties [PROP_TITLE] = g_param_spec_string ("title", "Title", "The title of the project", NULL, (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); /** * IdeContext:workdir: * * The "workdir" property is the best guess at the working directory for the * context. This may be discovered using a common parent if multiple files * are opened without a project. * * Since: 3.32 */ properties [PROP_WORKDIR] = g_param_spec_object ("workdir", "Working Directory", "The working directory for the project", G_TYPE_FILE, (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); /** * IdeContext::log: * @self: an #IdeContext * @severity: the log severity * @domain: the log domain * @message: the log message * * This signal is emitted when a log item has been added for the context. * * Since: 3.32 */ signals [LOG] = g_signal_new_class_handler ("log", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_CALLBACK (ide_context_real_log), NULL, NULL, NULL, G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE); } static void ide_context_init (IdeContext *self) { g_autoptr(IdeNotifications) notifs = NULL; self->workdir = g_file_new_for_path (g_get_home_dir ()); self->project_id = g_strdup ("empty"); self->title = g_strdup (_("Untitled")); notifs = ide_notifications_new (); ide_object_append (IDE_OBJECT (self), IDE_OBJECT (notifs)); } /** * ide_context_new: * * Creates a new #IdeContext. * * This only creates the context object. After creating the object you need * to set a number of properties and then initialize asynchronously using * g_async_initable_init_async(). * * Returns: (transfer full): an #IdeContext * * Since: 3.32 */ IdeContext * ide_context_new (void) { return ide_object_new (IDE_TYPE_CONTEXT, NULL); } static void ide_context_peek_child_typed_cb (IdeObject *object, gpointer user_data) { struct { IdeObject *ret; GType type; } *lookup = user_data; g_assert (IDE_IS_MAIN_THREAD ()); if (lookup->ret != NULL) return; /* Take a borrowed instance, we're in the main thread so * we can ensure it's not fully destroyed. */ if (G_TYPE_CHECK_INSTANCE_TYPE (object, lookup->type)) lookup->ret = object; } /** * ide_context_peek_child_typed: * @self: a #IdeContext * @type: the #GType of the child * * Looks for the first child matching @type, and returns it. No reference is * taken to the child, so you should avoid using this except as used by * compatability functions. * * This may only be called from the main thread or you risk the objects * being finalized before your caller has a chance to reference them. * * Returns: (transfer none) (type IdeObject) (nullable): an #IdeObject that * matches @type if successful; otherwise %NULL * * Since: 3.32 */ gpointer ide_context_peek_child_typed (IdeContext *self, GType type) { struct { IdeObject *ret; GType type; } lookup = { NULL, type }; g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); ide_object_lock (IDE_OBJECT (self)); ide_object_foreach (IDE_OBJECT (self), (GFunc)ide_context_peek_child_typed_cb, &lookup); ide_object_unlock (IDE_OBJECT (self)); return lookup.ret; } /** * ide_context_dup_project_id: * @self: a #IdeContext * * Copies the project-id and returns it to the caller. * * Returns: (transfer full): a project-id as a string * * Since: 3.32 */ gchar * ide_context_dup_project_id (IdeContext *self) { gchar *ret; g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); ide_object_lock (IDE_OBJECT (self)); ret = g_strdup (self->project_id); ide_object_unlock (IDE_OBJECT (self)); g_return_val_if_fail (ret != NULL, NULL); return g_steal_pointer (&ret); } /** * ide_context_set_project_id: * @self: a #IdeContext * * Sets the project-id for the context. * * Generally, this should only be done once after loading a project. * * Since: 3.32 */ void ide_context_set_project_id (IdeContext *self, const gchar *project_id) { g_return_if_fail (IDE_IS_CONTEXT (self)); if (ide_str_empty0 (project_id)) project_id = "empty"; ide_object_lock (IDE_OBJECT (self)); if (!ide_str_equal0 (self->project_id, project_id)) { g_free (self->project_id); self->project_id = g_strdup (project_id); ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROJECT_ID]); } ide_object_unlock (IDE_OBJECT (self)); } /** * ide_context_ref_workdir: * @self: a #IdeContext * * Gets the working-directory of the context and increments the * reference count by one. * * Returns: (transfer full): a #GFile * * Since: 3.32 */ GFile * ide_context_ref_workdir (IdeContext *self) { GFile *ret; g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); ide_object_lock (IDE_OBJECT (self)); ret = g_object_ref (self->workdir); ide_object_unlock (IDE_OBJECT (self)); return g_steal_pointer (&ret); } /** * ide_context_set_workdir: * @self: a #IdeContext * @workdir: a #GFile * * Sets the working directory for the project. * * This should generally only be set once after checking out the project. * * In future releases, changes may be made to change this in support of * git-worktrees or similar workflows. * * Since: 3.32 */ void ide_context_set_workdir (IdeContext *self, GFile *workdir) { g_return_if_fail (IDE_IS_CONTEXT (self)); g_return_if_fail (G_IS_FILE (workdir)); ide_object_lock (IDE_OBJECT (self)); if (g_set_object (&self->workdir, workdir)) ide_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WORKDIR]); ide_object_unlock (IDE_OBJECT (self)); } /** * ide_context_cache_file: * @self: a #IdeContext * @first_part: (nullable): The first part of the path * * Like ide_context_cache_filename() but returns a #GFile. * * Returns: (transfer full): a #GFile for the cache file * * Since: 3.32 */ GFile * ide_context_cache_file (IdeContext *self, const gchar *first_part, ...) { g_autoptr(GPtrArray) ar = NULL; g_autofree gchar *path = NULL; g_autofree gchar *project_id = NULL; const gchar *part = first_part; va_list args; g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); project_id = ide_context_dup_project_id (self); ar = g_ptr_array_new (); g_ptr_array_add (ar, (gchar *)g_get_user_cache_dir ()); g_ptr_array_add (ar, (gchar *)ide_get_program_name ()); g_ptr_array_add (ar, (gchar *)"projects"); g_ptr_array_add (ar, (gchar *)project_id); if (part != NULL) { va_start (args, first_part); do { g_ptr_array_add (ar, (gchar *)part); part = va_arg (args, const gchar *); } while (part != NULL); va_end (args); } g_ptr_array_add (ar, NULL); path = g_build_filenamev ((gchar **)ar->pdata); return g_file_new_for_path (path); } /** * ide_context_cache_filename: * @self: a #IdeContext * @first_part: the first part of the filename * * Creates a new filename that will be located in the projects cache directory. * This makes it convenient to remove files when a project is deleted as all * cache files will share a unified parent directory. * * The file will be located in a directory similar to * ~/.cache/gnome-builder/project_name. This may change based on the value * of g_get_user_cache_dir(). * * Returns: (transfer full): A new string containing the cache filename * * Since: 3.32 */ gchar * ide_context_cache_filename (IdeContext *self, const gchar *first_part, ...) { g_autofree gchar *project_id = NULL; g_autofree gchar *base = NULL; va_list args; gchar *ret; g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); project_id = ide_context_dup_project_id (self); g_return_val_if_fail (project_id != NULL, NULL); base = g_build_filename (g_get_user_cache_dir (), ide_get_program_name (), "projects", project_id, first_part, NULL); if (first_part != NULL) { va_start (args, first_part); ret = g_build_filename_valist (base, &args); va_end (args); } else { ret = g_steal_pointer (&base); } return g_steal_pointer (&ret); } /** * ide_context_build_file: * @self: a #IdeContext * @path: (nullable): a path to the file * * Creates a new #GFile for the path. * * - If @path is %NULL, #IdeContext:workdir is returned. * - If @path is absolute, a new #GFile to the absolute path is returned. * - Otherwise, a #GFile child of #IdeContext:workdir is returned. * * Returns: (transfer full): a #GFile * * Since: 3.32 */ GFile * ide_context_build_file (IdeContext *self, const gchar *path) { g_autoptr(GFile) ret = NULL; g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); if (path == NULL) ret = g_file_dup (self->workdir); else if (g_path_is_absolute (path)) ret = g_file_new_for_path (path); else ret = g_file_get_child (self->workdir, path); g_debug ("Creating file \"%s\" from \"%s\"", g_file_peek_path (ret), path); return g_steal_pointer (&ret); } /** * ide_context_build_filename: * @self: a #IdeContext * @first_part: first path part * * Creates a new path that starts from the working directory of the * loaded project. * * Returns: (transfer full): a string containing the new path * * Since: 3.32 */ gchar * ide_context_build_filename (IdeContext *self, const gchar *first_part, ...) { g_autoptr(GPtrArray) ar = NULL; g_autoptr(GFile) workdir = NULL; const gchar *part = first_part; const gchar *base; va_list args; g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); g_return_val_if_fail (first_part != NULL, NULL); workdir = ide_context_ref_workdir (self); base = g_file_peek_path (workdir); ar = g_ptr_array_new (); /* If first part is absolute, just use that as our root */ if (!g_path_is_absolute (first_part)) g_ptr_array_add (ar, (gchar *)base); va_start (args, first_part); do { g_ptr_array_add (ar, (gchar *)part); part = va_arg (args, const gchar *); } while (part != NULL); va_end (args); g_ptr_array_add (ar, NULL); return g_build_filenamev ((gchar **)ar->pdata); } /** * ide_context_ref_project_settings: * @self: a #IdeContext * * Gets an org.gnome.builder.project #GSettings. * * This creates a new #GSettings instance for the project. * * Returns: (transfer full): a #GSettings * * Since: 3.32 */ GSettings * ide_context_ref_project_settings (IdeContext *self) { g_autofree gchar *path = NULL; g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); ide_object_lock (IDE_OBJECT (self)); path = g_strdup_printf ("/org/gnome/builder/projects/%s/", self->project_id); ide_object_unlock (IDE_OBJECT (self)); return g_settings_new_with_path ("org.gnome.builder.project", path); } /** * ide_context_dup_title: * @self: a #IdeContext * * Returns: (transfer full): a string containing the title * * Since: 3.32 */ gchar * ide_context_dup_title (IdeContext *self) { gchar *ret; g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL); ide_object_lock (IDE_OBJECT (self)); ret = g_strdup (self->title); ide_object_unlock (IDE_OBJECT (self)); return g_steal_pointer (&ret); } /** * ide_context_set_title: * @self: an #IdeContext * @title: (nullable): the title for the project or %NULL * * Sets the #IdeContext:title property. This is used by various * components to show the user the name of the project. This may * include the omnibar and the window title. * * Since: 3.32 */ void ide_context_set_title (IdeContext *self, const gchar *title) { g_return_if_fail (IDE_IS_CONTEXT (self)); if (ide_str_empty0 (title)) title = _("Untitled"); ide_object_lock (IDE_OBJECT (self)); if (!ide_str_equal0 (self->title, title)) { g_free (self->title); self->title = g_strdup (title); ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_TITLE]); } ide_object_unlock (IDE_OBJECT (self)); } void ide_context_log (IdeContext *self, GLogLevelFlags level, const gchar *domain, const gchar *message) { g_assert (IDE_IS_CONTEXT (self)); g_signal_emit (self, signals [LOG], 0, level, domain, message); } /** * ide_context_has_project: * @self: a #IdeContext * * Checks to see if a project has been loaded in @context. * * Returns: %TRUE if a project has been, or is currently, loading. * * Since: 3.32 */ gboolean ide_context_has_project (IdeContext *self) { gboolean ret; g_return_val_if_fail (IDE_IS_CONTEXT (self), FALSE); ide_object_lock (IDE_OBJECT (self)); ret = self->project_loaded; ide_object_unlock (IDE_OBJECT (self)); return ret; } void _ide_context_set_has_project (IdeContext *self) { g_return_if_fail (IDE_IS_CONTEXT (self)); ide_object_lock (IDE_OBJECT (self)); self->project_loaded = TRUE; ide_object_unlock (IDE_OBJECT (self)); } /** * ide_context_addin_find_by_module_name: * @context: an #IdeContext * @module_name: the name of the addin module * * Finds the addin (if any) matching the plugin's @module_name. * * Returns: (transfer none) (nullable): an #IdeContextAddin or %NULL * * Since: 3.40 */ IdeContextAddin * ide_context_addin_find_by_module_name (IdeContext *context, const gchar *module_name) { PeasPluginInfo *plugin_info; PeasExtension *ret = NULL; PeasEngine *engine; g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL); g_return_val_if_fail (module_name != NULL, NULL); if (context->addins == NULL) return NULL; engine = peas_engine_get_default (); if ((plugin_info = peas_engine_get_plugin_info (engine, module_name))) ret = peas_extension_set_get_extension (context->addins, plugin_info); return IDE_CONTEXT_ADDIN (ret); }