1 /* ide-context.c
2  *
3  * Copyright 2014-2019 Christian Hergert <chergert@redhat.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * SPDX-License-Identifier: GPL-3.0-or-later
19  */
20 
21 #define G_LOG_DOMAIN "ide-context"
22 
23 #include "config.h"
24 
25 #include <glib/gi18n.h>
26 #include <libpeas/peas.h>
27 
28 #include "ide-context.h"
29 #include "ide-context-private.h"
30 #include "ide-context-addin.h"
31 #include "ide-macros.h"
32 #include "ide-notifications.h"
33 
34 /**
35  * SECTION:ide-context
36  * @title: IdeContext
37  * @short_description: the root object for a project
38  *
39  * The #IdeContext object is the root object for a project. Everything
40  * in a project is contained by this object.
41  *
42  * Since: 3.32
43  */
44 
45 struct _IdeContext
46 {
47   IdeObject         parent_instance;
48   PeasExtensionSet *addins;
49   gchar            *project_id;
50   gchar            *title;
51   GFile            *workdir;
52   guint             project_loaded : 1;
53 };
54 
55 enum {
56   PROP_0,
57   PROP_PROJECT_ID,
58   PROP_TITLE,
59   PROP_WORKDIR,
60   N_PROPS
61 };
62 
63 enum {
64   LOG,
65   N_SIGNALS
66 };
67 
G_DEFINE_FINAL_TYPE(IdeContext,ide_context,IDE_TYPE_OBJECT)68 G_DEFINE_FINAL_TYPE (IdeContext, ide_context, IDE_TYPE_OBJECT)
69 
70 static GParamSpec *properties [N_PROPS];
71 static guint signals [N_SIGNALS];
72 
73 static void
74 ide_context_addin_load_project_cb (GObject      *object,
75                                    GAsyncResult *result,
76                                    gpointer      user_data)
77 {
78   IdeContextAddin *addin = (IdeContextAddin *)object;
79   g_autoptr(IdeContext) self = user_data;
80   g_autoptr(GError) error = NULL;
81 
82   g_assert (IDE_IS_CONTEXT_ADDIN (addin));
83   g_assert (G_IS_ASYNC_RESULT (result));
84   g_assert (IDE_IS_CONTEXT (self));
85 
86   if (ide_context_addin_load_project_finish (addin, result, &error))
87     ide_context_addin_project_loaded (addin, self);
88   else
89     g_warning ("%s context addin failed to load project: %s",
90                G_OBJECT_TYPE_NAME (addin), error->message);
91 }
92 
93 static void
ide_context_addin_added_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)94 ide_context_addin_added_cb (PeasExtensionSet *set,
95                             PeasPluginInfo   *plugin_info,
96                             PeasExtension    *exten,
97                             gpointer          user_data)
98 {
99   IdeContextAddin *addin = (IdeContextAddin *)exten;
100   IdeContext *self = user_data;
101   g_autoptr(GCancellable) cancellable = NULL;
102 
103   g_assert (PEAS_IS_EXTENSION_SET (set));
104   g_assert (plugin_info != NULL);
105   g_assert (IDE_IS_CONTEXT_ADDIN (addin));
106 
107   /* Ignore any request during shutdown */
108   cancellable = ide_object_ref_cancellable (IDE_OBJECT (self));
109   if (g_cancellable_is_cancelled (cancellable))
110     return;
111 
112   ide_context_addin_load (addin, self);
113 
114   if (self->project_loaded)
115     ide_context_addin_load_project_async (addin,
116                                           self,
117                                           cancellable,
118                                           ide_context_addin_load_project_cb,
119                                           g_object_ref (self));
120 }
121 
122 static void
ide_context_addin_removed_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)123 ide_context_addin_removed_cb (PeasExtensionSet *set,
124                               PeasPluginInfo   *plugin_info,
125                               PeasExtension    *exten,
126                               gpointer          user_data)
127 {
128   IdeContextAddin *addin = (IdeContextAddin *)exten;
129   IdeContext *self = user_data;
130 
131   g_assert (PEAS_IS_EXTENSION_SET (set));
132   g_assert (plugin_info != NULL);
133   g_assert (IDE_IS_CONTEXT_ADDIN (addin));
134 
135   ide_context_addin_unload (addin, self);
136 }
137 
138 static void
ide_context_real_log(IdeContext * self,GLogLevelFlags level,const gchar * domain,const gchar * message)139 ide_context_real_log (IdeContext     *self,
140                       GLogLevelFlags  level,
141                       const gchar    *domain,
142                       const gchar    *message)
143 {
144   g_log (domain, level, "%s", message);
145 }
146 
147 static gchar *
ide_context_repr(IdeObject * object)148 ide_context_repr (IdeObject *object)
149 {
150   IdeContext *self = IDE_CONTEXT (object);
151 
152   return g_strdup_printf ("%s workdir=\"%s\" has_project=%d",
153                           G_OBJECT_TYPE_NAME (self),
154                           g_file_peek_path (self->workdir),
155                           self->project_loaded);
156 }
157 
158 static void
ide_context_constructed(GObject * object)159 ide_context_constructed (GObject *object)
160 {
161   IdeContext *self = (IdeContext *)object;
162 
163   g_assert (IDE_IS_OBJECT (object));
164 
165   self->addins = peas_extension_set_new (peas_engine_get_default (),
166                                          IDE_TYPE_CONTEXT_ADDIN,
167                                          NULL);
168 
169   g_signal_connect (self->addins,
170                     "extension-added",
171                     G_CALLBACK (ide_context_addin_added_cb),
172                     self);
173 
174   g_signal_connect (self->addins,
175                     "extension-removed",
176                     G_CALLBACK (ide_context_addin_removed_cb),
177                     self);
178 
179   peas_extension_set_foreach (self->addins,
180                               ide_context_addin_added_cb,
181                               self);
182 
183   G_OBJECT_CLASS (ide_context_parent_class)->constructed (object);
184 }
185 
186 static void
ide_context_destroy(IdeObject * object)187 ide_context_destroy (IdeObject *object)
188 {
189   IdeContext *self = (IdeContext *)object;
190 
191   g_assert (IDE_IS_OBJECT (object));
192 
193   g_clear_object (&self->addins);
194 
195   IDE_OBJECT_CLASS (ide_context_parent_class)->destroy (object);
196 }
197 
198 static void
ide_context_finalize(GObject * object)199 ide_context_finalize (GObject *object)
200 {
201   IdeContext *self = (IdeContext *)object;
202 
203   g_clear_object (&self->workdir);
204   g_clear_pointer (&self->project_id, g_free);
205   g_clear_pointer (&self->title, g_free);
206 
207   G_OBJECT_CLASS (ide_context_parent_class)->finalize (object);
208 }
209 
210 static void
ide_context_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)211 ide_context_get_property (GObject    *object,
212                           guint       prop_id,
213                           GValue     *value,
214                           GParamSpec *pspec)
215 {
216   IdeContext *self = IDE_CONTEXT (object);
217 
218   switch (prop_id)
219     {
220     case PROP_PROJECT_ID:
221       g_value_take_string (value, ide_context_dup_project_id (self));
222       break;
223 
224     case PROP_TITLE:
225       g_value_take_string (value, ide_context_dup_title (self));
226       break;
227 
228     case PROP_WORKDIR:
229       g_value_take_object (value, ide_context_ref_workdir (self));
230       break;
231 
232     default:
233       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
234     }
235 }
236 
237 static void
ide_context_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)238 ide_context_set_property (GObject      *object,
239                           guint         prop_id,
240                           const GValue *value,
241                           GParamSpec   *pspec)
242 {
243   IdeContext *self = IDE_CONTEXT (object);
244 
245   switch (prop_id)
246     {
247     case PROP_PROJECT_ID:
248       ide_context_set_project_id (self, g_value_get_string (value));
249       break;
250 
251     case PROP_TITLE:
252       ide_context_set_title (self, g_value_get_string (value));
253       break;
254 
255     case PROP_WORKDIR:
256       ide_context_set_workdir (self, g_value_get_object (value));
257       break;
258 
259     default:
260       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
261     }
262 }
263 
264 static void
ide_context_class_init(IdeContextClass * klass)265 ide_context_class_init (IdeContextClass *klass)
266 {
267   GObjectClass *object_class = G_OBJECT_CLASS (klass);
268   IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
269 
270   object_class->constructed = ide_context_constructed;
271   object_class->finalize = ide_context_finalize;
272   object_class->get_property = ide_context_get_property;
273   object_class->set_property = ide_context_set_property;
274 
275   i_object_class->destroy = ide_context_destroy;
276   i_object_class->repr = ide_context_repr;
277 
278   /**
279    * IdeContext:project-id:
280    *
281    * The "project-id" property is the identifier to use when creating
282    * files and folders for this project. It has a mutated form of either
283    * the directory or some other discoverable trait of the project.
284    *
285    * It has also been modified to remove spaces and other unsafe
286    * characters for file-systems.
287    *
288    * This may change during runtime, but usually only once when the
289    * project has been initialize loaded.
290    *
291    * Before any project has loaded, this is "empty" to allow flexibility
292    * for non-project use.
293    *
294    * Since: 3.32
295    */
296   properties [PROP_PROJECT_ID] =
297     g_param_spec_string ("project-id",
298                          "Project Id",
299                          "The project identifier used when creating files and folders",
300                          "empty",
301                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
302 
303   /**
304    * IdeContext:title:
305    *
306    * The "title" property is a descriptive name for the project.
307    *
308    * Since: 3.32
309    */
310   properties [PROP_TITLE] =
311     g_param_spec_string ("title",
312                          "Title",
313                          "The title of the project",
314                          NULL,
315                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
316 
317   /**
318    * IdeContext:workdir:
319    *
320    * The "workdir" property is the best guess at the working directory for the
321    * context. This may be discovered using a common parent if multiple files
322    * are opened without a project.
323    *
324    * Since: 3.32
325    */
326   properties [PROP_WORKDIR] =
327     g_param_spec_object ("workdir",
328                          "Working Directory",
329                          "The working directory for the project",
330                          G_TYPE_FILE,
331                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
332 
333   g_object_class_install_properties (object_class, N_PROPS, properties);
334 
335   /**
336    * IdeContext::log:
337    * @self: an #IdeContext
338    * @severity: the log severity
339    * @domain: the log domain
340    * @message: the log message
341    *
342    * This signal is emitted when a log item has been added for the context.
343    *
344    * Since: 3.32
345    */
346   signals [LOG] =
347     g_signal_new_class_handler ("log",
348                                 G_TYPE_FROM_CLASS (klass),
349                                 G_SIGNAL_RUN_LAST,
350                                 G_CALLBACK (ide_context_real_log),
351                                 NULL, NULL, NULL,
352                                 G_TYPE_NONE,
353                                 3,
354                                 G_TYPE_UINT,
355                                 G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
356                                 G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
357 }
358 
359 static void
ide_context_init(IdeContext * self)360 ide_context_init (IdeContext *self)
361 {
362   g_autoptr(IdeNotifications) notifs = NULL;
363 
364   self->workdir = g_file_new_for_path (g_get_home_dir ());
365   self->project_id = g_strdup ("empty");
366   self->title = g_strdup (_("Untitled"));
367 
368   notifs = ide_notifications_new ();
369   ide_object_append (IDE_OBJECT (self), IDE_OBJECT (notifs));
370 }
371 
372 /**
373  * ide_context_new:
374  *
375  * Creates a new #IdeContext.
376  *
377  * This only creates the context object. After creating the object you need
378  * to set a number of properties and then initialize asynchronously using
379  * g_async_initable_init_async().
380  *
381  * Returns: (transfer full): an #IdeContext
382  *
383  * Since: 3.32
384  */
385 IdeContext *
ide_context_new(void)386 ide_context_new (void)
387 {
388   return ide_object_new (IDE_TYPE_CONTEXT, NULL);
389 }
390 
391 static void
ide_context_peek_child_typed_cb(IdeObject * object,gpointer user_data)392 ide_context_peek_child_typed_cb (IdeObject *object,
393                                  gpointer   user_data)
394 {
395   struct {
396     IdeObject *ret;
397     GType      type;
398   } *lookup = user_data;
399 
400   g_assert (IDE_IS_MAIN_THREAD ());
401 
402   if (lookup->ret != NULL)
403     return;
404 
405   /* Take a borrowed instance, we're in the main thread so
406    * we can ensure it's not fully destroyed.
407    */
408   if (G_TYPE_CHECK_INSTANCE_TYPE (object, lookup->type))
409     lookup->ret = object;
410 }
411 
412 /**
413  * ide_context_peek_child_typed:
414  * @self: a #IdeContext
415  * @type: the #GType of the child
416  *
417  * Looks for the first child matching @type, and returns it. No reference is
418  * taken to the child, so you should avoid using this except as used by
419  * compatability functions.
420  *
421  * This may only be called from the main thread or you risk the objects
422  * being finalized before your caller has a chance to reference them.
423  *
424  * Returns: (transfer none) (type IdeObject) (nullable): an #IdeObject that
425  *   matches @type if successful; otherwise %NULL
426  *
427  * Since: 3.32
428  */
429 gpointer
ide_context_peek_child_typed(IdeContext * self,GType type)430 ide_context_peek_child_typed (IdeContext *self,
431                               GType       type)
432 {
433   struct {
434     IdeObject *ret;
435     GType      type;
436   } lookup = { NULL, type };
437 
438   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
439   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
440 
441   ide_object_lock (IDE_OBJECT (self));
442   ide_object_foreach (IDE_OBJECT (self), (GFunc)ide_context_peek_child_typed_cb, &lookup);
443   ide_object_unlock (IDE_OBJECT (self));
444 
445   return lookup.ret;
446 }
447 
448 /**
449  * ide_context_dup_project_id:
450  * @self: a #IdeContext
451  *
452  * Copies the project-id and returns it to the caller.
453  *
454  * Returns: (transfer full): a project-id as a string
455  *
456  * Since: 3.32
457  */
458 gchar *
ide_context_dup_project_id(IdeContext * self)459 ide_context_dup_project_id (IdeContext *self)
460 {
461   gchar *ret;
462 
463   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
464 
465   ide_object_lock (IDE_OBJECT (self));
466   ret = g_strdup (self->project_id);
467   ide_object_unlock (IDE_OBJECT (self));
468 
469   g_return_val_if_fail (ret != NULL, NULL);
470 
471   return g_steal_pointer (&ret);
472 }
473 
474 /**
475  * ide_context_set_project_id:
476  * @self: a #IdeContext
477  *
478  * Sets the project-id for the context.
479  *
480  * Generally, this should only be done once after loading a project.
481  *
482  * Since: 3.32
483  */
484 void
ide_context_set_project_id(IdeContext * self,const gchar * project_id)485 ide_context_set_project_id (IdeContext  *self,
486                             const gchar *project_id)
487 {
488   g_return_if_fail (IDE_IS_CONTEXT (self));
489 
490   if (ide_str_empty0 (project_id))
491     project_id = "empty";
492 
493   ide_object_lock (IDE_OBJECT (self));
494   if (!ide_str_equal0 (self->project_id, project_id))
495     {
496       g_free (self->project_id);
497       self->project_id = g_strdup (project_id);
498       ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROJECT_ID]);
499     }
500   ide_object_unlock (IDE_OBJECT (self));
501 }
502 
503 /**
504  * ide_context_ref_workdir:
505  * @self: a #IdeContext
506  *
507  * Gets the working-directory of the context and increments the
508  * reference count by one.
509  *
510  * Returns: (transfer full): a #GFile
511  *
512  * Since: 3.32
513  */
514 GFile *
ide_context_ref_workdir(IdeContext * self)515 ide_context_ref_workdir (IdeContext *self)
516 {
517   GFile *ret;
518 
519   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
520 
521   ide_object_lock (IDE_OBJECT (self));
522   ret = g_object_ref (self->workdir);
523   ide_object_unlock (IDE_OBJECT (self));
524 
525   return g_steal_pointer (&ret);
526 }
527 
528 /**
529  * ide_context_set_workdir:
530  * @self: a #IdeContext
531  * @workdir: a #GFile
532  *
533  * Sets the working directory for the project.
534  *
535  * This should generally only be set once after checking out the project.
536  *
537  * In future releases, changes may be made to change this in support of
538  * git-worktrees or similar workflows.
539  *
540  * Since: 3.32
541  */
542 void
ide_context_set_workdir(IdeContext * self,GFile * workdir)543 ide_context_set_workdir (IdeContext *self,
544                          GFile      *workdir)
545 {
546   g_return_if_fail (IDE_IS_CONTEXT (self));
547   g_return_if_fail (G_IS_FILE (workdir));
548 
549   ide_object_lock (IDE_OBJECT (self));
550   if (g_set_object (&self->workdir, workdir))
551     ide_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WORKDIR]);
552   ide_object_unlock (IDE_OBJECT (self));
553 }
554 
555 /**
556  * ide_context_cache_file:
557  * @self: a #IdeContext
558  * @first_part: (nullable): The first part of the path
559  *
560  * Like ide_context_cache_filename() but returns a #GFile.
561  *
562  * Returns: (transfer full): a #GFile for the cache file
563  *
564  * Since: 3.32
565  */
566 GFile *
ide_context_cache_file(IdeContext * self,const gchar * first_part,...)567 ide_context_cache_file (IdeContext  *self,
568                         const gchar *first_part,
569                         ...)
570 {
571   g_autoptr(GPtrArray) ar = NULL;
572   g_autofree gchar *path = NULL;
573   g_autofree gchar *project_id = NULL;
574   const gchar *part = first_part;
575   va_list args;
576 
577   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
578 
579   project_id = ide_context_dup_project_id (self);
580 
581   ar = g_ptr_array_new ();
582   g_ptr_array_add (ar, (gchar *)g_get_user_cache_dir ());
583   g_ptr_array_add (ar, (gchar *)ide_get_program_name ());
584   g_ptr_array_add (ar, (gchar *)"projects");
585   g_ptr_array_add (ar, (gchar *)project_id);
586 
587   if (part != NULL)
588     {
589       va_start (args, first_part);
590       do
591         {
592           g_ptr_array_add (ar, (gchar *)part);
593           part = va_arg (args, const gchar *);
594         }
595       while (part != NULL);
596       va_end (args);
597     }
598 
599   g_ptr_array_add (ar, NULL);
600 
601   path = g_build_filenamev ((gchar **)ar->pdata);
602 
603   return g_file_new_for_path (path);
604 }
605 
606 /**
607  * ide_context_cache_filename:
608  * @self: a #IdeContext
609  * @first_part: the first part of the filename
610  *
611  * Creates a new filename that will be located in the projects cache directory.
612  * This makes it convenient to remove files when a project is deleted as all
613  * cache files will share a unified parent directory.
614  *
615  * The file will be located in a directory similar to
616  * ~/.cache/gnome-builder/project_name. This may change based on the value
617  * of g_get_user_cache_dir().
618  *
619  * Returns: (transfer full): A new string containing the cache filename
620  *
621  * Since: 3.32
622  */
623 gchar *
ide_context_cache_filename(IdeContext * self,const gchar * first_part,...)624 ide_context_cache_filename (IdeContext  *self,
625                             const gchar *first_part,
626                             ...)
627 {
628   g_autofree gchar *project_id = NULL;
629   g_autofree gchar *base = NULL;
630   va_list args;
631   gchar *ret;
632 
633   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
634 
635   project_id = ide_context_dup_project_id (self);
636 
637   g_return_val_if_fail (project_id != NULL, NULL);
638 
639   base = g_build_filename (g_get_user_cache_dir (),
640                            ide_get_program_name (),
641                            "projects",
642                            project_id,
643                            first_part,
644                            NULL);
645 
646   if (first_part != NULL)
647     {
648       va_start (args, first_part);
649       ret = g_build_filename_valist (base, &args);
650       va_end (args);
651     }
652   else
653     {
654       ret = g_steal_pointer (&base);
655     }
656 
657   return g_steal_pointer (&ret);
658 }
659 
660 /**
661  * ide_context_build_file:
662  * @self: a #IdeContext
663  * @path: (nullable): a path to the file
664  *
665  * Creates a new #GFile for the path.
666  *
667  * - If @path is %NULL, #IdeContext:workdir is returned.
668  * - If @path is absolute, a new #GFile to the absolute path is returned.
669  * - Otherwise, a #GFile child of #IdeContext:workdir is returned.
670  *
671  * Returns: (transfer full): a #GFile
672  *
673  * Since: 3.32
674  */
675 GFile *
ide_context_build_file(IdeContext * self,const gchar * path)676 ide_context_build_file (IdeContext  *self,
677                         const gchar *path)
678 {
679   g_autoptr(GFile) ret = NULL;
680 
681   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
682 
683   if (path == NULL)
684     ret = g_file_dup (self->workdir);
685   else if (g_path_is_absolute (path))
686     ret = g_file_new_for_path (path);
687   else
688     ret = g_file_get_child (self->workdir, path);
689 
690   g_debug ("Creating file \"%s\" from \"%s\"", g_file_peek_path (ret), path);
691 
692   return g_steal_pointer (&ret);
693 }
694 
695 /**
696  * ide_context_build_filename:
697  * @self: a #IdeContext
698  * @first_part: first path part
699  *
700  * Creates a new path that starts from the working directory of the
701  * loaded project.
702  *
703  * Returns: (transfer full): a string containing the new path
704  *
705  * Since: 3.32
706  */
707 gchar *
ide_context_build_filename(IdeContext * self,const gchar * first_part,...)708 ide_context_build_filename (IdeContext  *self,
709                             const gchar *first_part,
710                             ...)
711 {
712   g_autoptr(GPtrArray) ar = NULL;
713   g_autoptr(GFile) workdir = NULL;
714   const gchar *part = first_part;
715   const gchar *base;
716   va_list args;
717 
718   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
719   g_return_val_if_fail (first_part != NULL, NULL);
720 
721   workdir = ide_context_ref_workdir (self);
722   base = g_file_peek_path (workdir);
723 
724   ar = g_ptr_array_new ();
725 
726   /* If first part is absolute, just use that as our root */
727   if (!g_path_is_absolute (first_part))
728     g_ptr_array_add (ar, (gchar *)base);
729 
730   va_start (args, first_part);
731   do
732     {
733       g_ptr_array_add (ar, (gchar *)part);
734       part = va_arg (args, const gchar *);
735     }
736   while (part != NULL);
737   va_end (args);
738 
739   g_ptr_array_add (ar, NULL);
740 
741   return g_build_filenamev ((gchar **)ar->pdata);
742 }
743 
744 /**
745  * ide_context_ref_project_settings:
746  * @self: a #IdeContext
747  *
748  * Gets an org.gnome.builder.project #GSettings.
749  *
750  * This creates a new #GSettings instance for the project.
751  *
752  * Returns: (transfer full): a #GSettings
753  *
754  * Since: 3.32
755  */
756 GSettings *
ide_context_ref_project_settings(IdeContext * self)757 ide_context_ref_project_settings (IdeContext *self)
758 {
759   g_autofree gchar *path = NULL;
760 
761   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
762 
763   ide_object_lock (IDE_OBJECT (self));
764   path = g_strdup_printf ("/org/gnome/builder/projects/%s/", self->project_id);
765   ide_object_unlock (IDE_OBJECT (self));
766 
767   return g_settings_new_with_path ("org.gnome.builder.project", path);
768 }
769 
770 /**
771  * ide_context_dup_title:
772  * @self: a #IdeContext
773  *
774  * Returns: (transfer full): a string containing the title
775  *
776  * Since: 3.32
777  */
778 gchar *
ide_context_dup_title(IdeContext * self)779 ide_context_dup_title (IdeContext *self)
780 {
781   gchar *ret;
782 
783   g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
784 
785   ide_object_lock (IDE_OBJECT (self));
786   ret = g_strdup (self->title);
787   ide_object_unlock (IDE_OBJECT (self));
788 
789   return g_steal_pointer (&ret);
790 }
791 
792 /**
793  * ide_context_set_title:
794  * @self: an #IdeContext
795  * @title: (nullable): the title for the project or %NULL
796  *
797  * Sets the #IdeContext:title property. This is used by various
798  * components to show the user the name of the project. This may
799  * include the omnibar and the window title.
800  *
801  * Since: 3.32
802  */
803 void
ide_context_set_title(IdeContext * self,const gchar * title)804 ide_context_set_title (IdeContext  *self,
805                        const gchar *title)
806 {
807   g_return_if_fail (IDE_IS_CONTEXT (self));
808 
809   if (ide_str_empty0 (title))
810     title = _("Untitled");
811 
812   ide_object_lock (IDE_OBJECT (self));
813   if (!ide_str_equal0 (self->title, title))
814     {
815       g_free (self->title);
816       self->title = g_strdup (title);
817       ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_TITLE]);
818     }
819   ide_object_unlock (IDE_OBJECT (self));
820 }
821 
822 void
ide_context_log(IdeContext * self,GLogLevelFlags level,const gchar * domain,const gchar * message)823 ide_context_log (IdeContext     *self,
824                  GLogLevelFlags  level,
825                  const gchar    *domain,
826                  const gchar    *message)
827 {
828   g_assert (IDE_IS_CONTEXT (self));
829 
830   g_signal_emit (self, signals [LOG], 0, level, domain, message);
831 }
832 
833 /**
834  * ide_context_has_project:
835  * @self: a #IdeContext
836  *
837  * Checks to see if a project has been loaded in @context.
838  *
839  * Returns: %TRUE if a project has been, or is currently, loading.
840  *
841  * Since: 3.32
842  */
843 gboolean
ide_context_has_project(IdeContext * self)844 ide_context_has_project (IdeContext *self)
845 {
846   gboolean ret;
847 
848   g_return_val_if_fail (IDE_IS_CONTEXT (self), FALSE);
849 
850   ide_object_lock (IDE_OBJECT (self));
851   ret = self->project_loaded;
852   ide_object_unlock (IDE_OBJECT (self));
853 
854   return ret;
855 }
856 
857 void
_ide_context_set_has_project(IdeContext * self)858 _ide_context_set_has_project (IdeContext *self)
859 {
860   g_return_if_fail (IDE_IS_CONTEXT (self));
861 
862   ide_object_lock (IDE_OBJECT (self));
863   self->project_loaded = TRUE;
864   ide_object_unlock (IDE_OBJECT (self));
865 }
866 
867 /**
868  * ide_context_addin_find_by_module_name:
869  * @context: an #IdeContext
870  * @module_name: the name of the addin module
871  *
872  * Finds the addin (if any) matching the plugin's @module_name.
873  *
874  * Returns: (transfer none) (nullable): an #IdeContextAddin or %NULL
875  *
876  * Since: 3.40
877  */
878 IdeContextAddin *
ide_context_addin_find_by_module_name(IdeContext * context,const gchar * module_name)879 ide_context_addin_find_by_module_name (IdeContext  *context,
880                                        const gchar *module_name)
881 {
882   PeasPluginInfo *plugin_info;
883   PeasExtension *ret = NULL;
884   PeasEngine *engine;
885 
886   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
887   g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
888   g_return_val_if_fail (module_name != NULL, NULL);
889 
890   if (context->addins == NULL)
891     return NULL;
892 
893   engine = peas_engine_get_default ();
894 
895   if ((plugin_info = peas_engine_get_plugin_info (engine, module_name)))
896     ret = peas_extension_set_get_extension (context->addins, plugin_info);
897 
898   return IDE_CONTEXT_ADDIN (ret);
899 }
900