1 /* ide-workbench.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-workbench"
22 
23 #include "config.h"
24 
25 #include <glib/gi18n.h>
26 #include <libide-debugger.h>
27 #include <libide-threading.h>
28 #include <libpeas/peas.h>
29 
30 #include "ide-build-private.h"
31 #include "ide-context-private.h"
32 #include "ide-foundry-init.h"
33 #include "ide-thread-private.h"
34 #include "ide-transfer-manager-private.h"
35 
36 #include "ide-application.h"
37 #include "ide-command-manager.h"
38 #include "ide-gui-global.h"
39 #include "ide-gui-private.h"
40 #include "ide-primary-workspace.h"
41 #include "ide-workbench.h"
42 #include "ide-workbench-addin.h"
43 #include "ide-workspace.h"
44 
45 /**
46  * SECTION:ide-workbench
47  * @title: IdeWorkbench
48  * @short_description: window group for all windows within a project
49  *
50  * The #IdeWorkbench is a #GtkWindowGroup containing the #IdeContext (root
51  * data-structure for a project) and all of the windows associated with the
52  * project.
53  *
54  * Usually, windows within the #IdeWorkbench are an #IdeWorkspace. They can
55  * react to changes in the #IdeContext or its descendants to represent the
56  * project and it's state.
57  *
58  * Since: 3.32
59  */
60 
61 struct _IdeWorkbench
62 {
63   GtkWindowGroup    parent_instance;
64 
65   /* MRU of workspaces, link embedded in workspace */
66   GQueue            mru_queue;
67 
68   /* Owned references */
69   PeasExtensionSet *addins;
70   GCancellable     *cancellable;
71   IdeContext       *context;
72   IdeBuildSystem   *build_system;
73   IdeProjectInfo   *project_info;
74   IdeVcs           *vcs;
75   IdeVcsMonitor    *vcs_monitor;
76   IdeSearchEngine  *search_engine;
77 
78   /* Various flags */
79   guint             unloaded : 1;
80 };
81 
82 typedef struct
83 {
84   GPtrArray          *addins;
85   IdeWorkbenchAddin  *preferred;
86   GFile              *file;
87   gchar              *hint;
88   gchar              *content_type;
89   IdeBufferOpenFlags  flags;
90   gint                at_line;
91   gint                at_line_offset;
92 } Open;
93 
94 typedef struct
95 {
96   IdeProjectInfo *project_info;
97   GPtrArray      *addins;
98   GType           workspace_type;
99   gint64          present_time;
100 } LoadProject;
101 
102 typedef struct
103 {
104   GPtrArray *roots;
105   gchar     *path;
106 } ResolveFile;
107 
108 enum {
109   PROP_0,
110   PROP_CONTEXT,
111   PROP_VCS,
112   N_PROPS
113 };
114 
115 static void ide_workbench_action_close       (IdeWorkbench *self,
116                                               GVariant     *param);
117 static void ide_workbench_action_open        (IdeWorkbench *self,
118                                               GVariant     *param);
119 static void ide_workbench_action_dump_tasks  (IdeWorkbench *self,
120                                               GVariant     *param);
121 static void ide_workbench_action_object_tree (IdeWorkbench *self,
122                                               GVariant     *param);
123 static void ide_workbench_action_inspector   (IdeWorkbench *self,
124                                               GVariant     *param);
125 static void ide_workbench_action_reload_all  (IdeWorkbench *self,
126                                               GVariant     *param);
127 
128 
129 DZL_DEFINE_ACTION_GROUP (IdeWorkbench, ide_workbench, {
130   { "close", ide_workbench_action_close },
131   { "open", ide_workbench_action_open },
132   { "reload-files", ide_workbench_action_reload_all },
133   { "-inspector", ide_workbench_action_inspector },
134   { "-object-tree", ide_workbench_action_object_tree },
135   { "-dump-tasks", ide_workbench_action_dump_tasks },
136 })
137 
138 G_DEFINE_FINAL_TYPE_WITH_CODE (IdeWorkbench, ide_workbench, GTK_TYPE_WINDOW_GROUP,
139                          G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
140                                                 ide_workbench_init_action_group))
141 
142 static GParamSpec *properties [N_PROPS];
143 
144 static void
load_project_free(LoadProject * lp)145 load_project_free (LoadProject *lp)
146 {
147   g_clear_object (&lp->project_info);
148   g_clear_pointer (&lp->addins, g_ptr_array_unref);
149   g_slice_free (LoadProject, lp);
150 }
151 
152 static void
open_free(Open * o)153 open_free (Open *o)
154 {
155   g_clear_pointer (&o->addins, g_ptr_array_unref);
156   g_clear_object (&o->preferred);
157   g_clear_object (&o->file);
158   g_clear_pointer (&o->hint, g_free);
159   g_clear_pointer (&o->content_type, g_free);
160   g_slice_free (Open, o);
161 }
162 
163 static void
resolve_file_free(ResolveFile * rf)164 resolve_file_free (ResolveFile *rf)
165 {
166   g_clear_pointer (&rf->roots, g_ptr_array_unref);
167   g_clear_pointer (&rf->path, g_free);
168   g_slice_free (ResolveFile, rf);
169 }
170 
171 static gboolean
ignore_error(GError * error)172 ignore_error (GError *error)
173 {
174   return g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
175          g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED);
176 }
177 
178 /**
179  * ide_workbench_from_context:
180  * @context: an #IdeContext
181  *
182  * Helper to get the #IdeWorkbench for a given context.
183  *
184  * Returns: (transfer none) (nullable): an #IdeWorkbench or %NULL
185  *
186  * Since: 3.40
187  */
188 IdeWorkbench *
ide_workbench_from_context(IdeContext * context)189 ide_workbench_from_context (IdeContext *context)
190 {
191   g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
192 
193   return IDE_WORKBENCH (g_object_get_data (G_OBJECT (context), "WORKBENCH"));
194 }
195 
196 static void
ide_workbench_set_context(IdeWorkbench * self,IdeContext * context)197 ide_workbench_set_context (IdeWorkbench *self,
198                            IdeContext   *context)
199 {
200   g_autoptr(IdeContext) new_context = NULL;
201   g_autoptr(IdeBufferManager) bufmgr = NULL;
202   IdeBuildSystem *build_system;
203 
204   g_return_if_fail (IDE_IS_WORKBENCH (self));
205   g_return_if_fail (!context || IDE_IS_CONTEXT (context));
206 
207   if (context == NULL)
208     context = new_context = ide_context_new ();
209 
210   /* backpointer for the workbench */
211   g_object_set_data (G_OBJECT (context), "WORKBENCH", self);
212 
213   g_set_object (&self->context, context);
214 
215   /* Make sure we have access to buffer manager early */
216   bufmgr = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_BUFFER_MANAGER);
217 
218   /* And use a fallback build system if one is not already available */
219   if ((build_system = ide_context_peek_child_typed (context, IDE_TYPE_BUILD_SYSTEM)))
220     self->build_system = g_object_ref (build_system);
221   else
222     self->build_system = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_FALLBACK_BUILD_SYSTEM);
223 }
224 
225 static void
ide_workbench_addin_added_workspace_cb(IdeWorkspace * workspace,IdeWorkbenchAddin * addin)226 ide_workbench_addin_added_workspace_cb (IdeWorkspace      *workspace,
227                                         IdeWorkbenchAddin *addin)
228 {
229   g_assert (IDE_IS_WORKSPACE (workspace));
230   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
231 
232   ide_workbench_addin_workspace_added (addin, workspace);
233 }
234 
235 static void
ide_workbench_addin_removed_workspace_cb(IdeWorkspace * workspace,IdeWorkbenchAddin * addin)236 ide_workbench_addin_removed_workspace_cb (IdeWorkspace      *workspace,
237                                           IdeWorkbenchAddin *addin)
238 {
239   g_assert (IDE_IS_WORKSPACE (workspace));
240   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
241 
242   ide_workbench_addin_workspace_removed (addin, workspace);
243 }
244 
245 static void
ide_workbench_addin_added_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)246 ide_workbench_addin_added_cb (PeasExtensionSet *set,
247                               PeasPluginInfo   *plugin_info,
248                               PeasExtension    *exten,
249                               gpointer          user_data)
250 {
251   IdeWorkbench *self = user_data;
252   IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
253 
254   g_assert (PEAS_IS_EXTENSION_SET (set));
255   g_assert (plugin_info != NULL);
256   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
257   g_assert (IDE_IS_WORKBENCH (self));
258 
259   ide_workbench_addin_load (addin, self);
260 
261   /* Notify of the VCS system up-front */
262   if (self->vcs != NULL)
263     ide_workbench_addin_vcs_changed (addin, self->vcs);
264 
265   /*
266    * If we already loaded a project, then give the plugin a
267    * chance to handle that, even if it is delayed a bit.
268    */
269 
270   if (self->project_info != NULL)
271     ide_workbench_addin_load_project_async (addin, self->project_info, NULL, NULL, NULL);
272 
273   ide_workbench_foreach_workspace (self,
274                                    (GtkCallback)ide_workbench_addin_added_workspace_cb,
275                                    addin);
276 }
277 
278 static void
ide_workbench_addin_removed_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)279 ide_workbench_addin_removed_cb (PeasExtensionSet *set,
280                                 PeasPluginInfo   *plugin_info,
281                                 PeasExtension    *exten,
282                                 gpointer          user_data)
283 {
284   IdeWorkbench *self = user_data;
285   IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
286 
287   g_assert (PEAS_IS_EXTENSION_SET (set));
288   g_assert (plugin_info != NULL);
289   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
290   g_assert (IDE_IS_WORKBENCH (self));
291 
292   /* Notify of workspace removals so addins don't need to manually
293    * track them for cleanup.
294    */
295   ide_workbench_foreach_workspace (self,
296                                    (GtkCallback)ide_workbench_addin_removed_workspace_cb,
297                                    addin);
298 
299   ide_workbench_addin_unload (addin, self);
300 }
301 
302 static void
ide_workbench_notify_context_title(IdeWorkbench * self,GParamSpec * pspec,IdeContext * context)303 ide_workbench_notify_context_title (IdeWorkbench *self,
304                                     GParamSpec   *pspec,
305                                     IdeContext   *context)
306 {
307   g_autofree gchar *formatted = NULL;
308   g_autofree gchar *title = NULL;
309 
310   g_assert (IDE_IS_MAIN_THREAD ());
311   g_assert (IDE_IS_WORKBENCH (self));
312   g_assert (IDE_IS_CONTEXT (context));
313 
314   title = ide_context_dup_title (context);
315   formatted = g_strdup_printf (_("Builder — %s"), title);
316   ide_workbench_foreach_workspace (self,
317                                    (GtkCallback)gtk_window_set_title,
318                                    formatted);
319 }
320 
321 static void
ide_workbench_notify_context_workdir(IdeWorkbench * self,GParamSpec * pspec,IdeContext * context)322 ide_workbench_notify_context_workdir (IdeWorkbench *self,
323                                       GParamSpec   *pspec,
324                                       IdeContext   *context)
325 {
326   g_autoptr(GFile) workdir = NULL;
327 
328   g_assert (IDE_IS_MAIN_THREAD ());
329   g_assert (IDE_IS_WORKBENCH (self));
330   g_assert (IDE_IS_CONTEXT (context));
331 
332   workdir = ide_context_ref_workdir (context);
333   ide_vcs_monitor_set_root (self->vcs_monitor, workdir);
334 }
335 
336 static void
ide_workbench_constructed(GObject * object)337 ide_workbench_constructed (GObject *object)
338 {
339   IdeWorkbench *self = (IdeWorkbench *)object;
340 
341   g_assert (IDE_IS_WORKBENCH (self));
342 
343   if (self->context == NULL)
344     self->context = ide_context_new ();
345 
346   g_signal_connect_object (self->context,
347                            "notify::title",
348                            G_CALLBACK (ide_workbench_notify_context_title),
349                            self,
350                            G_CONNECT_SWAPPED);
351 
352   g_signal_connect_object (self->context,
353                            "notify::workdir",
354                            G_CALLBACK (ide_workbench_notify_context_workdir),
355                            self,
356                            G_CONNECT_SWAPPED);
357 
358   G_OBJECT_CLASS (ide_workbench_parent_class)->constructed (object);
359 
360   self->vcs_monitor = g_object_new (IDE_TYPE_VCS_MONITOR,
361                                     "parent", self->context,
362                                     NULL);
363 
364   self->addins = peas_extension_set_new (peas_engine_get_default (),
365                                          IDE_TYPE_WORKBENCH_ADDIN,
366                                          NULL);
367 
368   g_signal_connect (self->addins,
369                     "extension-added",
370                     G_CALLBACK (ide_workbench_addin_added_cb),
371                     self);
372 
373   g_signal_connect (self->addins,
374                     "extension-removed",
375                     G_CALLBACK (ide_workbench_addin_removed_cb),
376                     self);
377 
378   peas_extension_set_foreach (self->addins,
379                               ide_workbench_addin_added_cb,
380                               self);
381 
382   /* Load command providers (which may register shortcuts) */
383   (void)ide_command_manager_from_context (self->context);
384 }
385 
386 static void
ide_workbench_finalize(GObject * object)387 ide_workbench_finalize (GObject *object)
388 {
389   IdeWorkbench *self = (IdeWorkbench *)object;
390 
391   g_assert (IDE_IS_MAIN_THREAD ());
392 
393   if (self->context != NULL)
394     g_object_set_data (G_OBJECT (self->context), "WORKBENCH", NULL);
395 
396   g_clear_object (&self->build_system);
397   g_clear_object (&self->vcs);
398   g_clear_object (&self->search_engine);
399   g_clear_object (&self->project_info);
400   g_clear_object (&self->cancellable);
401   g_clear_object (&self->context);
402 
403   G_OBJECT_CLASS (ide_workbench_parent_class)->finalize (object);
404 }
405 
406 static void
ide_workbench_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)407 ide_workbench_get_property (GObject    *object,
408                             guint       prop_id,
409                             GValue     *value,
410                             GParamSpec *pspec)
411 {
412   IdeWorkbench *self = IDE_WORKBENCH (object);
413 
414   g_assert (IDE_IS_MAIN_THREAD ());
415 
416   switch (prop_id)
417     {
418     case PROP_CONTEXT:
419       g_value_set_object (value, ide_workbench_get_context (self));
420       break;
421 
422     default:
423       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
424     }
425 }
426 
427 static void
ide_workbench_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)428 ide_workbench_set_property (GObject      *object,
429                             guint         prop_id,
430                             const GValue *value,
431                             GParamSpec   *pspec)
432 {
433   IdeWorkbench *self = IDE_WORKBENCH (object);
434 
435   g_assert (IDE_IS_MAIN_THREAD ());
436 
437   switch (prop_id)
438     {
439     case PROP_CONTEXT:
440       ide_workbench_set_context (self, g_value_get_object (value));
441       break;
442 
443     default:
444       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
445     }
446 }
447 
448 static void
ide_workbench_class_init(IdeWorkbenchClass * klass)449 ide_workbench_class_init (IdeWorkbenchClass *klass)
450 {
451   GObjectClass *object_class = G_OBJECT_CLASS (klass);
452 
453   object_class->constructed = ide_workbench_constructed;
454   object_class->finalize = ide_workbench_finalize;
455   object_class->get_property = ide_workbench_get_property;
456   object_class->set_property = ide_workbench_set_property;
457 
458   /**
459    * IdeWorkbench:context:
460    *
461    * The "context" property is the #IdeContext for the project.
462    *
463    * The #IdeContext is the root #IdeObject used in the tree of
464    * objects representing the project and the workings of the IDE.
465    *
466    * Since: 3.32
467    */
468   properties [PROP_CONTEXT] =
469     g_param_spec_object ("context",
470                          "Context",
471                          "The IdeContext for the workbench",
472                          IDE_TYPE_CONTEXT,
473                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
474 
475   /**
476    * IdeWorkbench:vcs:
477    *
478    * The "vcs" property contains an #IdeVcs that represents the version control
479    * system that is currently loaded for the project.
480    *
481    * The #IdeVcs is registered by an #IdeWorkbenchAddin when loading a project.
482    *
483    * Since: 3.32
484    */
485   properties [PROP_VCS] =
486     g_param_spec_object ("vcs",
487                          "Vcs",
488                          "The version control system, if any",
489                          IDE_TYPE_VCS,
490                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
491 
492   g_object_class_install_properties (object_class, N_PROPS, properties);
493 }
494 
495 static void
ide_workbench_init(IdeWorkbench * self)496 ide_workbench_init (IdeWorkbench *self)
497 {
498 }
499 
500 static void
collect_addins_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)501 collect_addins_cb (PeasExtensionSet *set,
502                    PeasPluginInfo   *plugin_info,
503                    PeasExtension    *exten,
504                    gpointer          user_data)
505 {
506   GPtrArray *ar = user_data;
507   g_ptr_array_add (ar, g_object_ref (exten));
508 }
509 
510 static GPtrArray *
ide_workbench_collect_addins(IdeWorkbench * self)511 ide_workbench_collect_addins (IdeWorkbench *self)
512 {
513   g_autoptr(GPtrArray) ar = NULL;
514 
515   g_assert (IDE_IS_WORKBENCH (self));
516 
517   ar = g_ptr_array_new_with_free_func (g_object_unref);
518   if (self->addins != NULL)
519     peas_extension_set_foreach (self->addins, collect_addins_cb, ar);
520   return g_steal_pointer (&ar);
521 }
522 
523 static IdeWorkbenchAddin *
ide_workbench_find_addin(IdeWorkbench * self,const gchar * hint)524 ide_workbench_find_addin (IdeWorkbench *self,
525                           const gchar  *hint)
526 {
527   PeasEngine *engine;
528   PeasPluginInfo *plugin_info;
529   PeasExtension *exten = NULL;
530 
531   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
532   g_return_val_if_fail (hint != NULL, NULL);
533 
534   engine = peas_engine_get_default ();
535 
536   if ((plugin_info = peas_engine_get_plugin_info (engine, hint)))
537     exten = peas_extension_set_get_extension (self->addins, plugin_info);
538 
539   return exten ? g_object_ref (IDE_WORKBENCH_ADDIN (exten)) : NULL;
540 }
541 
542 /**
543  * ide_workbench_new:
544  *
545  * Creates a new #IdeWorkbench.
546  *
547  * This does not create any windows, you'll need to request that a workspace
548  * be created based on the kind of workspace you want to display to the user.
549  *
550  * Returns: an #IdeWorkbench
551  *
552  * Since: 3.32
553  */
554 IdeWorkbench *
ide_workbench_new(void)555 ide_workbench_new (void)
556 {
557   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
558 
559   return g_object_new (IDE_TYPE_WORKBENCH, NULL);
560 }
561 
562 /**
563  * ide_workbench_new_for_context:
564  *
565  * Creates a new #IdeWorkbench using @context for the #IdeWorkbench:context.
566  *
567  * Returns: (transfer full): an #IdeWorkbench
568  *
569  * Since: 3.32
570  */
571 IdeWorkbench *
ide_workbench_new_for_context(IdeContext * context)572 ide_workbench_new_for_context (IdeContext *context)
573 {
574   g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
575 
576   return g_object_new (IDE_TYPE_CONTEXT,
577                        "visible", TRUE,
578                        NULL);
579 }
580 
581 /**
582  * ide_workbench_get_context:
583  * @self: an #IdeWorkbench
584  *
585  * Gets the #IdeContext for the workbench.
586  *
587  * Returns: (transfer none): an #IdeContext
588  *
589  * Since: 3.32
590  */
591 IdeContext *
ide_workbench_get_context(IdeWorkbench * self)592 ide_workbench_get_context (IdeWorkbench *self)
593 {
594   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
595   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
596 
597   return self->context;
598 }
599 
600 /**
601  * ide_workbench_from_widget:
602  * @widget: a #GtkWidget
603  *
604  * Finds the #IdeWorkbench associated with a widget.
605  *
606  * Returns: (nullable) (transfer none): an #IdeWorkbench or %NULL
607  *
608  * Since: 3.32
609  */
610 IdeWorkbench *
ide_workbench_from_widget(GtkWidget * widget)611 ide_workbench_from_widget (GtkWidget *widget)
612 {
613   GtkWindowGroup *group;
614   GtkWidget *toplevel;
615 
616   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
617   g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
618 
619   /*
620    * The workbench is a window group, and the workspaces belong to us. So we
621    * just need to get the toplevel window group property, and cast.
622    */
623 
624   if ((toplevel = gtk_widget_get_toplevel (widget)) &&
625       GTK_IS_WINDOW (toplevel) &&
626       (group = gtk_window_get_group (GTK_WINDOW (toplevel))) &&
627       IDE_IS_WORKBENCH (group))
628     return IDE_WORKBENCH (group);
629 
630   return NULL;
631 }
632 
633 /**
634  * ide_workbench_foreach_workspace:
635  * @self: an #IdeWorkbench
636  * @callback: (scope call): a #GtkCallback to call for each #IdeWorkspace
637  * @user_data: user data for @callback
638  *
639  * Iterates the available workspaces in the workbench. Workspaces are iterated
640  * in most-recently-used order.
641  *
642  * Since: 3.32
643  */
644 void
ide_workbench_foreach_workspace(IdeWorkbench * self,GtkCallback callback,gpointer user_data)645 ide_workbench_foreach_workspace (IdeWorkbench *self,
646                                  GtkCallback   callback,
647                                  gpointer      user_data)
648 {
649   GList *copy;
650 
651   g_return_if_fail (IDE_IS_MAIN_THREAD ());
652   g_return_if_fail (IDE_IS_WORKBENCH (self));
653   g_return_if_fail (callback != NULL);
654 
655   /* Copy for re-entrancy safety */
656   copy = g_list_copy (self->mru_queue.head);
657 
658   for (const GList *iter = copy; iter; iter = iter->next)
659     {
660       IdeWorkspace *workspace = iter->data;
661       g_assert (IDE_IS_WORKSPACE (workspace));
662       callback (GTK_WIDGET (workspace), user_data);
663     }
664 
665   g_list_free (copy);
666 }
667 
668 /**
669  * ide_workbench_foreach_page:
670  * @self: a #IdeWorkbench
671  * @callback: (scope call): a callback to execute for each page
672  * @user_data: closure data for @callback
673  *
674  * Calls @callback for every page loaded in the workbench, by iterating
675  * workspaces in order of most-recently-used.
676  *
677  * Since: 3.32
678  */
679 void
ide_workbench_foreach_page(IdeWorkbench * self,GtkCallback callback,gpointer user_data)680 ide_workbench_foreach_page (IdeWorkbench *self,
681                             GtkCallback   callback,
682                             gpointer      user_data)
683 {
684   GList *copy;
685 
686   g_return_if_fail (IDE_IS_WORKBENCH (self));
687   g_return_if_fail (callback != NULL);
688 
689   /* Make a copy to be safe against auto-cleanup removals */
690   copy = g_list_copy (self->mru_queue.head);
691   for (const GList *iter = copy; iter; iter = iter->next)
692     {
693       IdeWorkspace *workspace = iter->data;
694       g_assert (IDE_IS_WORKSPACE (workspace));
695       ide_workspace_foreach_page (workspace, callback, user_data);
696     }
697   g_list_free (copy);
698 }
699 
700 static void
ide_workbench_workspace_has_toplevel_focus_cb(IdeWorkbench * self,GParamSpec * pspec,IdeWorkspace * workspace)701 ide_workbench_workspace_has_toplevel_focus_cb (IdeWorkbench *self,
702                                                GParamSpec   *pspec,
703                                                IdeWorkspace *workspace)
704 {
705   g_assert (IDE_IS_WORKBENCH (self));
706   g_assert (IDE_IS_WORKSPACE (workspace));
707   g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self));
708 
709   if (gtk_window_has_toplevel_focus (GTK_WINDOW (workspace)))
710     {
711       GList *mru_link = _ide_workspace_get_mru_link (workspace);
712 
713       g_queue_unlink (&self->mru_queue, mru_link);
714 
715       g_assert (mru_link->prev == NULL);
716       g_assert (mru_link->next == NULL);
717       g_assert (mru_link->data == (gpointer)workspace);
718 
719       g_queue_push_head_link (&self->mru_queue, mru_link);
720     }
721 }
722 
723 static void
insert_action_groups_foreach_cb(IdeWorkspace * workspace,gpointer user_data)724 insert_action_groups_foreach_cb (IdeWorkspace *workspace,
725                                  gpointer      user_data)
726 {
727   IdeWorkbench *self = user_data;
728   struct {
729     const gchar *name;
730     GType        child_type;
731   } groups[] = {
732     { "config-manager", IDE_TYPE_CONFIG_MANAGER },
733     { "build-manager", IDE_TYPE_BUILD_MANAGER },
734     { "device-manager", IDE_TYPE_DEVICE_MANAGER },
735     { "run-manager", IDE_TYPE_RUN_MANAGER },
736     { "test-manager", IDE_TYPE_TEST_MANAGER },
737   };
738 
739   g_assert (IDE_IS_MAIN_THREAD ());
740   g_assert (IDE_IS_WORKBENCH (self));
741   g_assert (IDE_IS_WORKSPACE (workspace));
742 
743   for (guint i = 0; i < G_N_ELEMENTS (groups); i++)
744     {
745       IdeObject *child;
746 
747       if ((child = ide_context_peek_child_typed (self->context, groups[i].child_type)))
748         gtk_widget_insert_action_group (GTK_WIDGET (workspace),
749                                         groups[i].name,
750                                         G_ACTION_GROUP (child));
751     }
752 }
753 
754 /**
755  * ide_workbench_add_workspace:
756  * @self: an #IdeWorkbench
757  * @workspace: an #IdeWorkspace
758  *
759  * Adds @workspace to @workbench.
760  *
761  * Since: 3.32
762  */
763 void
ide_workbench_add_workspace(IdeWorkbench * self,IdeWorkspace * workspace)764 ide_workbench_add_workspace (IdeWorkbench *self,
765                              IdeWorkspace *workspace)
766 {
767   g_autoptr(GPtrArray) addins = NULL;
768   IdeCommandManager *command_manager;
769   GList *mru_link;
770 
771   g_return_if_fail (IDE_IS_MAIN_THREAD ());
772   g_return_if_fail (IDE_IS_WORKBENCH (self));
773   g_return_if_fail (IDE_IS_WORKSPACE (workspace));
774 
775   /* Now add the window to the workspace (which takes no reference, as the
776    * window will take a reference back to us.
777    */
778   if (gtk_window_get_group (GTK_WINDOW (workspace)) != GTK_WINDOW_GROUP (self))
779     gtk_window_group_add_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace));
780 
781   g_assert (gtk_window_has_group (GTK_WINDOW (workspace)));
782   g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self));
783 
784   /* Now place the workspace into our MRU tracking */
785   mru_link = _ide_workspace_get_mru_link (workspace);
786 
787   /* New workspaces are expected to be displayed right away, so we can
788    * just push the window onto the head.
789    */
790   g_queue_push_head_link (&self->mru_queue, mru_link);
791 
792   /* Update the context for the workspace, even if we're not loaded,
793    * this IdeContext will be updated later.
794    */
795   _ide_workspace_set_context (workspace, self->context);
796 
797   /* This causes the workspace to get an additional reference to the group
798    * (which already happens from GtkWindow:group), but IdeWorkspace will
799    * remove itself in IdeWorkspace.destroy.
800    */
801   gtk_widget_insert_action_group (GTK_WIDGET (workspace),
802                                   "workbench",
803                                   G_ACTION_GROUP (self));
804 
805   /* Give the workspace access to all the action groups of the context that
806    * might be useful for them to access (debug-manager, run-manager, etc).
807    */
808   if (self->project_info != NULL)
809     insert_action_groups_foreach_cb (workspace, self);
810 
811   /* Track toplevel focus changes to maintain a most-recently-used queue. */
812   g_signal_connect_object (workspace,
813                            "notify::has-toplevel-focus",
814                            G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb),
815                            self,
816                            G_CONNECT_SWAPPED);
817 
818   /* Give access to transfer-manager */
819   gtk_widget_insert_action_group (GTK_WIDGET (workspace),
820                                   "transfer-manager",
821                                   _ide_transfer_manager_get_actions (NULL));
822 
823   /* Notify all the addins about the new workspace. */
824   if ((addins = ide_workbench_collect_addins (self)))
825     {
826       for (guint i = 0; i < addins->len; i++)
827         {
828           IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
829           ide_workbench_addin_workspace_added (addin, workspace);
830         }
831     }
832 
833   if (!gtk_window_get_title (GTK_WINDOW (workspace)))
834     {
835       g_autofree gchar *title = NULL;
836       g_autofree gchar *formatted = NULL;
837 
838       title = ide_context_dup_title (self->context);
839       formatted = g_strdup_printf (_("Builder — %s"), title);
840       gtk_window_set_title (GTK_WINDOW (workspace), formatted);
841     }
842 
843   /* Load shortcuts for commands */
844   command_manager = ide_command_manager_from_context (self->context);
845   _ide_command_manager_init_shortcuts (command_manager, workspace);
846 }
847 
848 /**
849  * ide_workbench_remove_workspace:
850  * @self: an #IdeWorkbench
851  * @workspace: an #IdeWorkspace
852  *
853  * Removes @workspace from @workbench.
854  *
855  * Since: 3.32
856  */
857 void
ide_workbench_remove_workspace(IdeWorkbench * self,IdeWorkspace * workspace)858 ide_workbench_remove_workspace (IdeWorkbench *self,
859                                 IdeWorkspace *workspace)
860 {
861   g_autoptr(GPtrArray) addins = NULL;
862   IdeCommandManager *command_manager;
863   GList *list;
864   GList *mru_link;
865   guint count = 0;
866 
867   g_return_if_fail (IDE_IS_MAIN_THREAD ());
868   g_return_if_fail (IDE_IS_WORKBENCH (self));
869   g_return_if_fail (IDE_IS_WORKSPACE (workspace));
870 
871   /* Stop tracking MRU changes */
872   mru_link = _ide_workspace_get_mru_link (workspace);
873   g_queue_unlink (&self->mru_queue, mru_link);
874   g_signal_handlers_disconnect_by_func (workspace,
875                                         G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb),
876                                         self);
877 
878   /* Remove any shortcuts that were registered by command providers */
879   command_manager = ide_command_manager_from_context (self->context);
880   _ide_command_manager_unload_shortcuts (command_manager, workspace);
881 
882   /* Notify all the addins about losing the workspace. */
883   if ((addins = ide_workbench_collect_addins (self)))
884     {
885       for (guint i = 0; i < addins->len; i++)
886         {
887           IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
888           ide_workbench_addin_workspace_removed (addin, workspace);
889         }
890     }
891 
892   /* Clear our action group (which drops an additional back-reference) */
893   gtk_widget_insert_action_group (GTK_WIDGET (workspace), "workbench", NULL);
894 
895   /* Only cleanup the group if it hasn't already been removed */
896   if (gtk_window_has_group (GTK_WINDOW (workspace)))
897     gtk_window_group_remove_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace));
898 
899   /*
900    * If this is our last workspace being closed, then we want to
901    * try to cleanup the workbench and shut things down.
902    */
903 
904   list = gtk_window_group_list_windows (GTK_WINDOW_GROUP (self));
905   for (const GList *iter = list; iter; iter = iter->next)
906     {
907       GtkWindow *window = iter->data;
908 
909       if (IDE_IS_WORKSPACE (window) && workspace != IDE_WORKSPACE (window))
910         count++;
911     }
912   g_list_free (list);
913 
914   /*
915    * If there are no more workspaces left, then we will want to also
916    * unload the workbench opportunistically, so that the application
917    * can exit cleanly.
918    */
919   if (count == 0 && self->unloaded == FALSE)
920     ide_workbench_unload_async (self, NULL, NULL, NULL);
921 }
922 
923 /**
924  * ide_workbench_focus_workspace:
925  * @self: an #IdeWorkbench
926  * @workspace: an #IdeWorkspace
927  *
928  * Requests that @workspace be raised in the windows of @self, and
929  * displayed to the user.
930  *
931  * Since: 3.32
932  */
933 void
ide_workbench_focus_workspace(IdeWorkbench * self,IdeWorkspace * workspace)934 ide_workbench_focus_workspace (IdeWorkbench *self,
935                                IdeWorkspace *workspace)
936 {
937   g_return_if_fail (IDE_IS_MAIN_THREAD ());
938   g_return_if_fail (IDE_IS_WORKBENCH (self));
939   g_return_if_fail (IDE_IS_WORKSPACE (workspace));
940 
941   ide_gtk_window_present (GTK_WINDOW (workspace));
942 }
943 
944 static void
ide_workbench_project_loaded_foreach_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)945 ide_workbench_project_loaded_foreach_cb (PeasExtensionSet *set,
946                                          PeasPluginInfo   *plugin_info,
947                                          PeasExtension    *exten,
948                                          gpointer          user_data)
949 {
950   IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
951   IdeWorkbench *self = user_data;
952 
953   g_assert (IDE_IS_MAIN_THREAD ());
954   g_assert (PEAS_IS_EXTENSION_SET (set));
955   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
956   g_assert (IDE_IS_WORKBENCH (self));
957   g_assert (IDE_IS_PROJECT_INFO (self->project_info));
958 
959   ide_workbench_addin_project_loaded (addin, self->project_info);
960 }
961 
962 static void
ide_workbench_load_project_completed(IdeWorkbench * self,IdeTask * task)963 ide_workbench_load_project_completed (IdeWorkbench *self,
964                                       IdeTask      *task)
965 {
966   IdeBuildManager *build_manager;
967   LoadProject *lp;
968 
969   g_assert (IDE_IS_WORKBENCH (self));
970   g_assert (IDE_IS_TASK (task));
971 
972   lp = ide_task_get_task_data (task);
973 
974   g_assert (lp != NULL);
975   g_assert (lp->addins != NULL);
976   g_assert (lp->addins->len == 0);
977 
978   /* If we did not get a VCS as part of the loading process, set the
979    * fallback VCS implementation.
980    */
981   if (self->vcs == NULL)
982     {
983       g_autoptr(GFile) workdir = ide_context_ref_workdir (self->context);
984       g_autoptr(IdeDirectoryVcs) vcs = ide_directory_vcs_new (workdir);
985 
986       ide_workbench_set_vcs (self, IDE_VCS (vcs));
987     }
988 
989   /* Create the search engine up-front */
990   if (self->search_engine == NULL)
991     self->search_engine = ide_object_ensure_child_typed (IDE_OBJECT (self->context),
992                                                          IDE_TYPE_SEARCH_ENGINE);
993 
994   if (lp->workspace_type != G_TYPE_INVALID)
995     {
996       IdeWorkspace *workspace;
997 
998       workspace = g_object_new (lp->workspace_type,
999                                 "application", IDE_APPLICATION_DEFAULT,
1000                                 NULL);
1001       ide_workbench_add_workspace (self, IDE_WORKSPACE (workspace));
1002       gtk_window_present_with_time (GTK_WINDOW (workspace), lp->present_time);
1003     }
1004 
1005   /* Give workspaces access to the various GActionGroups */
1006   ide_workbench_foreach_workspace (self,
1007                                    (GtkCallback)insert_action_groups_foreach_cb,
1008                                    self);
1009 
1010   /* Notify addins that projects have loaded */
1011   peas_extension_set_foreach (self->addins,
1012                               ide_workbench_project_loaded_foreach_cb,
1013                               self);
1014 
1015   /* Now that we have a workspace window for the project, we can allow
1016    * the build manager to start.
1017    */
1018   build_manager = ide_build_manager_from_context (self->context);
1019   _ide_build_manager_start (build_manager);
1020 
1021   ide_task_return_boolean (task, TRUE);
1022 }
1023 
1024 static void
ide_workbench_load_project_cb(GObject * object,GAsyncResult * result,gpointer user_data)1025 ide_workbench_load_project_cb (GObject      *object,
1026                                GAsyncResult *result,
1027                                gpointer      user_data)
1028 {
1029   IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
1030   g_autoptr(IdeTask) task = user_data;
1031   g_autoptr(GError) error = NULL;
1032   IdeWorkbench *self;
1033   LoadProject *lp;
1034 
1035   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
1036   g_assert (G_IS_ASYNC_RESULT (result));
1037   g_assert (IDE_IS_TASK (task));
1038 
1039   self = ide_task_get_source_object (task);
1040   lp = ide_task_get_task_data (task);
1041 
1042   g_assert (IDE_IS_WORKBENCH (self));
1043   g_assert (lp != NULL);
1044   g_assert (IDE_IS_PROJECT_INFO (lp->project_info));
1045   g_assert (lp->addins != NULL);
1046   g_assert (lp->addins->len > 0);
1047 
1048   if (!ide_workbench_addin_load_project_finish (addin, result, &error))
1049     {
1050       if (!ignore_error (error))
1051         g_warning ("%s addin failed to load project: %s",
1052                    G_OBJECT_TYPE_NAME (addin), error->message);
1053     }
1054 
1055   g_ptr_array_remove (lp->addins, addin);
1056 
1057   if (lp->addins->len == 0)
1058     ide_workbench_load_project_completed (self, task);
1059 }
1060 
1061 static void
ide_workbench_init_foundry_cb(GObject * object,GAsyncResult * result,gpointer user_data)1062 ide_workbench_init_foundry_cb (GObject      *object,
1063                                GAsyncResult *result,
1064                                gpointer      user_data)
1065 {
1066   g_autoptr(IdeTask) task = user_data;
1067   g_autoptr(GError) error = NULL;
1068   IdeWorkbench *self;
1069   GCancellable *cancellable;
1070   LoadProject *lp;
1071 
1072   g_assert (IDE_IS_MAIN_THREAD ());
1073   g_assert (G_IS_ASYNC_RESULT (result));
1074   g_assert (IDE_IS_TASK (task));
1075 
1076   if (!_ide_foundry_init_finish (result, &error))
1077     g_critical ("Failed to initialize foundry: %s", error->message);
1078 
1079   cancellable = ide_task_get_cancellable (task);
1080   self = ide_task_get_source_object (task);
1081   lp = ide_task_get_task_data (task);
1082 
1083   g_assert (IDE_IS_WORKBENCH (self));
1084   g_assert (lp != NULL);
1085   g_assert (lp->addins != NULL);
1086   g_assert (IDE_IS_PROJECT_INFO (lp->project_info));
1087 
1088   /* Now, we need to notify all of the workbench addins that we're
1089    * opening the project. Once they have all completed, we'll create the
1090    * new workspace window and attach it. That saves us the work of
1091    * rendering various frames of the during the intensive load process.
1092    */
1093 
1094 
1095   for (guint i = 0; i < lp->addins->len; i++)
1096     {
1097       IdeWorkbenchAddin *addin = g_ptr_array_index (lp->addins, i);
1098 
1099       ide_workbench_addin_load_project_async (addin,
1100                                               lp->project_info,
1101                                               cancellable,
1102                                               ide_workbench_load_project_cb,
1103                                               g_object_ref (task));
1104     }
1105 
1106   if (lp->addins->len == 0)
1107     ide_workbench_load_project_completed (self, task);
1108 }
1109 
1110 /**
1111  * ide_workbench_load_project_async:
1112  * @self: a #IdeWorkbench
1113  * @project_info: an #IdeProjectInfo describing the project to open
1114  * @cancellable: (nullable): a #GCancellable or %NULL
1115  * @callback: (nullable): a #GAsyncReadyCallback to execute upon completion
1116  * @user_data: user data for @callback
1117  *
1118  * Requests that a project be opened in the workbench.
1119  *
1120  * @project_info should contain enough information to discover and load the
1121  * project. Depending on the various fields of the #IdeProjectInfo,
1122  * different plugins may become active as part of loading the project.
1123  *
1124  * Note that this may only be called once for an #IdeWorkbench. If you need
1125  * to open a second project, you need to create and register a second
1126  * workbench first, and then open using that secondary workbench.
1127  *
1128  * @callback should call ide_workbench_load_project_finish() to obtain the
1129  * result of the open request.
1130  *
1131  * Since: 3.32
1132  */
1133 void
ide_workbench_load_project_async(IdeWorkbench * self,IdeProjectInfo * project_info,GType workspace_type,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1134 ide_workbench_load_project_async (IdeWorkbench        *self,
1135                                   IdeProjectInfo      *project_info,
1136                                   GType                workspace_type,
1137                                   GCancellable        *cancellable,
1138                                   GAsyncReadyCallback  callback,
1139                                   gpointer             user_data)
1140 {
1141   g_autoptr(IdeTask) task = NULL;
1142   g_autoptr(GFile) parent = NULL;
1143   g_autofree gchar *name = NULL;
1144   const gchar *project_id;
1145   LoadProject *lp;
1146   GFile *directory;
1147   GFile *file;
1148 
1149   IDE_ENTRY;
1150 
1151   g_return_if_fail (IDE_IS_MAIN_THREAD ());
1152   g_return_if_fail (IDE_IS_WORKBENCH (self));
1153   g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
1154   g_return_if_fail (workspace_type != IDE_TYPE_WORKSPACE);
1155   g_return_if_fail (workspace_type == G_TYPE_INVALID ||
1156                     g_type_is_a (workspace_type, IDE_TYPE_WORKSPACE));
1157   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
1158   g_return_if_fail (self->unloaded == FALSE);
1159 
1160   task = ide_task_new (self, cancellable, callback, user_data);
1161   ide_task_set_source_tag (task, ide_workbench_load_project_async);
1162 
1163   if (self->project_info != NULL)
1164     {
1165       ide_task_return_new_error (task,
1166                                  G_IO_ERROR,
1167                                  G_IO_ERROR_FAILED,
1168                                  "Cannot load project, a project is already loaded");
1169       IDE_EXIT;
1170     }
1171 
1172   _ide_context_set_has_project (self->context);
1173 
1174   g_set_object (&self->project_info, project_info);
1175 
1176   /* Update context project-id based on project-info */
1177   if ((project_id = ide_project_info_get_id (project_info)))
1178     {
1179       g_autofree gchar *generated = ide_create_project_id (project_id);
1180       ide_context_set_project_id (self->context, generated);
1181     }
1182 
1183   if (!ide_project_info_get_directory (project_info) &&
1184       !ide_project_info_get_file (project_info))
1185     {
1186       ide_task_return_new_error (task,
1187                                  G_IO_ERROR,
1188                                  G_IO_ERROR_NOT_FOUND,
1189                                  "No file or directory provided to load as project");
1190       IDE_EXIT;
1191     }
1192 
1193   /* Fallback to using directory as file if necessary */
1194   if (!(file = ide_project_info_get_file (project_info)))
1195     {
1196       file = ide_project_info_get_directory (project_info);
1197       g_assert (G_IS_FILE (file));
1198 
1199       ide_project_info_set_file (project_info, file);
1200     }
1201 
1202   /*
1203    * Track the directory root based on project info. If we didn't get a
1204    * directory set, then take the parent of the project file.
1205    */
1206 
1207   if ((directory = ide_project_info_get_directory (project_info)))
1208     {
1209       ide_context_set_workdir (self->context, directory);
1210     }
1211   else
1212     {
1213       if (g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_DIRECTORY)
1214         {
1215           ide_context_set_workdir (self->context, file);
1216           directory = file;
1217         }
1218       else
1219         {
1220           ide_context_set_workdir (self->context, (parent = g_file_get_parent (file)));
1221           directory = parent;
1222         }
1223 
1224       ide_project_info_set_directory (project_info, directory);
1225     }
1226 
1227   g_assert (G_IS_FILE (directory));
1228 
1229   name = g_file_get_basename (directory);
1230   ide_context_set_title (self->context, name);
1231 
1232   {
1233     GFile *pdir = ide_project_info_get_directory (project_info);
1234     GFile *pfile = ide_project_info_get_file (project_info);
1235     const gchar *pident = ide_project_info_get_id (project_info);
1236     const gchar *pname = ide_project_info_get_name (project_info);
1237 
1238     /* Log some information to help track down project loading issues. */
1239     g_debug ("Loading project");
1240     g_debug ("    id = %s", pname);
1241     g_debug ("  name = %s", pident);
1242     g_debug ("   dir = %s", g_file_peek_path (pdir));
1243     g_debug ("  file = %s", g_file_peek_path (pfile));
1244   }
1245 
1246   /* If there has not been a project name set, make the default matching
1247    * the directory name. A plugin may update the name with more information
1248    * based on .doap files, etc.
1249    */
1250   if (!ide_project_info_get_name (project_info))
1251     ide_project_info_set_name (project_info, name);
1252 
1253   /* Setup some information we're going to need later on when loading the
1254    * individual workbench addins (and then creating the workspace).
1255    */
1256   lp = g_slice_new0 (LoadProject);
1257   lp->project_info = g_object_ref (project_info);
1258   /* HACK: Workaround for lack of last event time */
1259   lp->present_time = g_get_monotonic_time () / 1000L;
1260   lp->addins = ide_workbench_collect_addins (self);
1261   lp->workspace_type = workspace_type;
1262   ide_task_set_task_data (task, lp, load_project_free);
1263 
1264   /*
1265    * Before we load any addins, we want to register the Foundry subsystems
1266    * such as the device manager, diagnostics engine, configurations, etc.
1267    * This makes sure that we have some basics setup before addins load.
1268    */
1269   _ide_foundry_init_async (self->context,
1270                            cancellable,
1271                            ide_workbench_init_foundry_cb,
1272                            g_steal_pointer (&task));
1273 
1274   IDE_EXIT;
1275 }
1276 
1277 /**
1278  * ide_workbench_load_project_finish:
1279  * @self: a #IdeWorkbench
1280  *
1281  * Completes an asynchronous request to open a project using
1282  * ide_workbench_load_project_async().
1283  *
1284  * Returns: %TRUE if the project was successfully opened; otherwise %FALSE
1285  *   and @error is set.
1286  *
1287  * Since: 3.32
1288  */
1289 gboolean
ide_workbench_load_project_finish(IdeWorkbench * self,GAsyncResult * result,GError ** error)1290 ide_workbench_load_project_finish (IdeWorkbench  *self,
1291                                    GAsyncResult  *result,
1292                                    GError       **error)
1293 {
1294   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
1295   g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
1296   g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
1297 
1298   return ide_task_propagate_boolean (IDE_TASK (result), error);
1299 }
1300 
1301 static void
print_object_tree(IdeObject * object,gpointer depthptr)1302 print_object_tree (IdeObject *object,
1303                    gpointer   depthptr)
1304 {
1305   gint depth = GPOINTER_TO_INT (depthptr);
1306   g_autofree gchar *space = g_strnfill (depth * 2, ' ');
1307   g_autofree gchar *info = ide_object_repr (object);
1308 
1309   g_print ("%s%s\n", space, info);
1310   ide_object_foreach (object,
1311                       (GFunc)print_object_tree,
1312                       GINT_TO_POINTER (depth + 1));
1313 }
1314 
1315 static void
ide_workbench_action_object_tree(IdeWorkbench * self,GVariant * param)1316 ide_workbench_action_object_tree (IdeWorkbench *self,
1317                                   GVariant     *param)
1318 {
1319   g_assert (IDE_IS_WORKBENCH (self));
1320 
1321   print_object_tree (IDE_OBJECT (self->context), NULL);
1322 }
1323 
1324 static void
ide_workbench_action_dump_tasks(IdeWorkbench * self,GVariant * param)1325 ide_workbench_action_dump_tasks (IdeWorkbench *self,
1326                                  GVariant     *param)
1327 {
1328   g_assert (IDE_IS_WORKBENCH (self));
1329 
1330   _ide_dump_tasks ();
1331 }
1332 
1333 static void
ide_workbench_action_inspector(IdeWorkbench * self,GVariant * param)1334 ide_workbench_action_inspector (IdeWorkbench *self,
1335                                 GVariant     *param)
1336 {
1337   gtk_window_set_interactive_debugging (TRUE);
1338 }
1339 
1340 static void
ide_workbench_action_close_cb(GObject * object,GAsyncResult * result,gpointer user_data)1341 ide_workbench_action_close_cb (GObject      *object,
1342                                GAsyncResult *result,
1343                                gpointer      user_data)
1344 {
1345   IdeWorkbench *self = (IdeWorkbench *)object;
1346 
1347   g_assert (IDE_IS_MAIN_THREAD ());
1348   g_assert (IDE_IS_WORKBENCH (self));
1349   g_assert (G_IS_ASYNC_RESULT (result));
1350   g_assert (user_data == NULL);
1351 
1352   if (ide_workbench_unload_finish (self, result, NULL))
1353     {
1354       IdeApplication *app = IDE_APPLICATION_DEFAULT;
1355       GtkWindow *active;
1356 
1357       if (!(active = gtk_application_get_active_window (GTK_APPLICATION (app))))
1358         g_application_activate (G_APPLICATION (app));
1359       else
1360         ide_gtk_window_present (active);
1361     }
1362 }
1363 
1364 static void
ide_workbench_action_close(IdeWorkbench * self,GVariant * param)1365 ide_workbench_action_close (IdeWorkbench *self,
1366                             GVariant     *param)
1367 {
1368   g_assert (IDE_IS_WORKBENCH (self));
1369   g_assert (param == NULL);
1370 
1371   if (self->unloaded == FALSE)
1372     ide_workbench_unload_async (self,
1373                                 NULL,
1374                                 ide_workbench_action_close_cb,
1375                                 NULL);
1376 }
1377 
1378 static void
ide_workbench_action_reload_all(IdeWorkbench * self,GVariant * param)1379 ide_workbench_action_reload_all (IdeWorkbench *self,
1380                                  GVariant     *param)
1381 {
1382   IdeBufferManager *bufmgr;
1383   IdeContext *context;
1384 
1385   g_assert (IDE_IS_WORKBENCH (self));
1386   g_assert (param == NULL);
1387 
1388   context = ide_workbench_get_context (self);
1389   bufmgr = ide_buffer_manager_from_context (context);
1390   ide_buffer_manager_reload_all_async (bufmgr, NULL, NULL, NULL);
1391 }
1392 
1393 static void
ide_workbench_action_open(IdeWorkbench * self,GVariant * param)1394 ide_workbench_action_open (IdeWorkbench *self,
1395                            GVariant     *param)
1396 {
1397   GtkFileChooserNative *chooser;
1398   IdeWorkspace *workspace;
1399   gint ret;
1400 
1401   g_assert (IDE_IS_WORKBENCH (self));
1402   g_assert (param == NULL);
1403 
1404   workspace = ide_workbench_get_current_workspace (self);
1405 
1406   chooser = gtk_file_chooser_native_new (_("Open File…"),
1407                                          GTK_WINDOW (workspace),
1408                                          GTK_FILE_CHOOSER_ACTION_OPEN,
1409                                          _("_Open"),
1410                                          _("_Cancel"));
1411   gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (chooser), FALSE);
1412   gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (chooser), FALSE);
1413   gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (chooser), TRUE);
1414 
1415   ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (chooser));
1416 
1417   if (ret == GTK_RESPONSE_ACCEPT)
1418     {
1419       g_autoslist(GFile) files = gtk_file_chooser_get_files (GTK_FILE_CHOOSER (chooser));
1420 
1421       for (const GSList *iter = files; iter; iter = iter->next)
1422         {
1423           GFile *file = iter->data;
1424 
1425           g_assert (G_IS_FILE (file));
1426 
1427           ide_workbench_open_async (self, file, NULL, 0, NULL, NULL, NULL);
1428         }
1429     }
1430 
1431   gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser));
1432 }
1433 
1434 /**
1435  * ide_workbench_get_search_engine:
1436  * @self: a #IdeWorkbench
1437  *
1438  * Gets the search engine for the workbench, if any.
1439  *
1440  * Returns: (transfer none): an #IdeSearchEngine
1441  *
1442  * Since: 3.32
1443  */
1444 IdeSearchEngine *
ide_workbench_get_search_engine(IdeWorkbench * self)1445 ide_workbench_get_search_engine (IdeWorkbench *self)
1446 {
1447   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
1448   g_return_val_if_fail (self->context != NULL, NULL);
1449 
1450   if (self->search_engine == NULL)
1451     self->search_engine = ide_object_ensure_child_typed (IDE_OBJECT (self->context),
1452                                                          IDE_TYPE_SEARCH_ENGINE);
1453 
1454   return self->search_engine;
1455 }
1456 
1457 /**
1458  * ide_workbench_get_project_info:
1459  * @self: a #IdeWorkbench
1460  *
1461  * Gets the #IdeProjectInfo for the workbench, if a project has been or is
1462  * currently, loading.
1463  *
1464  * Returns: (transfer none) (nullable): an #IdeProjectInfo or %NULL
1465  *
1466  * Since: 3.32
1467  */
1468 IdeProjectInfo *
ide_workbench_get_project_info(IdeWorkbench * self)1469 ide_workbench_get_project_info (IdeWorkbench *self)
1470 {
1471   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
1472 
1473   return self->project_info;
1474 }
1475 
1476 static void
ide_workbench_unload_foundry_cb(GObject * object,GAsyncResult * result,gpointer user_data)1477 ide_workbench_unload_foundry_cb (GObject      *object,
1478                                  GAsyncResult *result,
1479                                  gpointer      user_data)
1480 {
1481   g_autoptr(IdeTask) task = user_data;
1482   g_autoptr(GError) error = NULL;
1483   IdeWorkbench *self;
1484 
1485   g_assert (G_IS_ASYNC_RESULT (result));
1486   g_assert (IDE_IS_TASK (task));
1487 
1488   self = ide_task_get_source_object (task);
1489 
1490   if (!_ide_foundry_unload_finish (result, &error))
1491     ide_task_return_error (task, g_steal_pointer (&error));
1492   else
1493     ide_task_return_boolean (task, TRUE);
1494 
1495   if (self->context != NULL)
1496     {
1497       ide_object_destroy (IDE_OBJECT (self->context));
1498       g_clear_object (&self->context);
1499     }
1500 }
1501 
1502 static void
ide_workbench_unload_project_completed(IdeWorkbench * self,IdeTask * task)1503 ide_workbench_unload_project_completed (IdeWorkbench *self,
1504                                         IdeTask      *task)
1505 {
1506   g_assert (IDE_IS_WORKBENCH (self));
1507   g_assert (IDE_IS_TASK (task));
1508 
1509   g_clear_object (&self->addins);
1510   ide_workbench_foreach_workspace (self, (GtkCallback)gtk_widget_destroy, NULL);
1511 
1512   _ide_foundry_unload_async (self->context,
1513                              ide_task_get_cancellable (task),
1514                              ide_workbench_unload_foundry_cb,
1515                              g_object_ref (task));
1516 }
1517 static void
ide_workbench_unload_project_cb(GObject * object,GAsyncResult * result,gpointer user_data)1518 ide_workbench_unload_project_cb (GObject      *object,
1519                                  GAsyncResult *result,
1520                                  gpointer      user_data)
1521 {
1522   IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
1523   g_autoptr(IdeTask) task = user_data;
1524   g_autoptr(GError) error = NULL;
1525   IdeWorkbench *self;
1526   GPtrArray *addins;
1527 
1528   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
1529   g_assert (G_IS_ASYNC_RESULT (result));
1530   g_assert (IDE_IS_TASK (task));
1531 
1532   self = ide_task_get_source_object (task);
1533   addins = ide_task_get_task_data (task);
1534 
1535   g_assert (IDE_IS_WORKBENCH (self));
1536   g_assert (addins != NULL);
1537   g_assert (addins->len > 0);
1538 
1539   if (!ide_workbench_addin_unload_project_finish (addin, result, &error))
1540     {
1541       if (!ignore_error (error))
1542         g_warning ("%s failed to unload project: %s",
1543                    G_OBJECT_TYPE_NAME (addin), error->message);
1544     }
1545 
1546   g_ptr_array_remove (addins, addin);
1547 
1548   if (addins->len == 0)
1549     ide_workbench_unload_project_completed (self, task);
1550 }
1551 
1552 /**
1553  * ide_workbench_unload_async:
1554  * @self: an #IdeWorkbench
1555  * @cancellable: (nullable): a #GCancellable
1556  * @callback: a #GAsyncReadyCallback to execute upon completion
1557  * @user_data: closure data for @callback
1558  *
1559  * Asynchronously unloads the workbench.
1560  *
1561  * All #IdeWorkspace windows will be closed after calling this
1562  * function.
1563  *
1564  * Since: 3.32
1565  */
1566 void
ide_workbench_unload_async(IdeWorkbench * self,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1567 ide_workbench_unload_async (IdeWorkbench        *self,
1568                             GCancellable        *cancellable,
1569                             GAsyncReadyCallback  callback,
1570                             gpointer             user_data)
1571 {
1572   g_autoptr(IdeTask) task = NULL;
1573   g_autoptr(GPtrArray) addins = NULL;
1574   GApplication *app;
1575 
1576   g_return_if_fail (IDE_IS_WORKBENCH (self));
1577   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
1578 
1579   task = ide_task_new (self, cancellable, callback, user_data);
1580   ide_task_set_source_tag (task, ide_workbench_unload_async);
1581 
1582   if (self->unloaded)
1583     {
1584       ide_task_return_boolean (task, TRUE);
1585       return;
1586     }
1587 
1588   self->unloaded = TRUE;
1589 
1590   /* Keep the GApplication alive for the lifetime of the task */
1591   app = g_application_get_default ();
1592   g_signal_connect_object (task,
1593                            "notify::completed",
1594                            G_CALLBACK (g_application_release),
1595                            app,
1596                            G_CONNECT_SWAPPED);
1597   g_application_hold (app);
1598 
1599   /*
1600    * Remove our workbench from the application, so that no new
1601    * open-file requests can keep us alive while we're shutting
1602    * down.
1603    */
1604 
1605   ide_application_remove_workbench (IDE_APPLICATION (app), self);
1606 
1607   /* If we haven't loaded a project, then there is nothing to
1608    * do right now, just let ide_workbench_addin_unload() be called
1609    * when the workbench disposes.
1610    */
1611   if (self->project_info == NULL)
1612     {
1613       ide_workbench_unload_project_completed (self, g_steal_pointer (&task));
1614       return;
1615     }
1616 
1617   addins = ide_workbench_collect_addins (self);
1618   ide_task_set_task_data (task, g_ptr_array_ref (addins), g_ptr_array_unref);
1619 
1620   if (addins->len == 0)
1621     {
1622       ide_workbench_unload_project_completed (self, task);
1623       return;
1624     }
1625 
1626   for (guint i = 0; i < addins->len; i++)
1627     {
1628       IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
1629 
1630       ide_workbench_addin_unload_project_async (addin,
1631                                                 self->project_info,
1632                                                 ide_task_get_cancellable (task),
1633                                                 ide_workbench_unload_project_cb,
1634                                                 g_object_ref (task));
1635     }
1636 
1637   /* Since the g_steal_pointer() just before doesn't always run, ensure the
1638    * task isn't freed while it hasn't yet finished running asynchronously.
1639    */
1640   task = NULL;
1641 }
1642 
1643 /**
1644  * ide_workbench_unload_finish:
1645  * @self: an #IdeWorkbench
1646  * @result: a #GAsyncResult provided to callback
1647  * @error: a location for a #GError, or %NULL
1648 
1649  * Completes a request to unload the workbench.
1650  *
1651  * Returns: %TRUE if the workbench was unloaded successfully,
1652  *   otherwise %FALSE and @error is set.
1653  *
1654  * Since: 3.32
1655  */
1656 gboolean
ide_workbench_unload_finish(IdeWorkbench * self,GAsyncResult * result,GError ** error)1657 ide_workbench_unload_finish (IdeWorkbench *self,
1658                              GAsyncResult *result,
1659                              GError **error)
1660 {
1661   g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
1662   g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
1663 
1664   return ide_task_propagate_boolean (IDE_TASK (result), error);
1665 }
1666 
1667 static void
ide_workbench_open_all_cb(GObject * object,GAsyncResult * result,gpointer user_data)1668 ide_workbench_open_all_cb (GObject      *object,
1669                            GAsyncResult *result,
1670                            gpointer      user_data)
1671 {
1672   IdeWorkbench *self = (IdeWorkbench *)object;
1673   g_autoptr(IdeTask) task = user_data;
1674   g_autoptr(GError) error = NULL;
1675   gint *n_active;
1676 
1677   g_assert (IDE_IS_WORKBENCH (self));
1678   g_assert (G_IS_ASYNC_RESULT (result));
1679   g_assert (IDE_IS_TASK (task));
1680 
1681   if (!ide_workbench_open_finish (self, result, &error))
1682     g_message ("Failed to open file: %s", error->message);
1683 
1684   n_active = ide_task_get_task_data (task);
1685   g_assert (n_active != NULL);
1686 
1687   (*n_active)--;
1688 
1689   if (*n_active == 0)
1690     ide_task_return_boolean (task, TRUE);
1691 }
1692 
1693 /**
1694  * ide_workbench_open_all_async:
1695  * @self: an #IdeWorkbench
1696  * @files: (array length=n_files): an array of #GFile
1697  * @n_files: number of #GFiles contained in @files
1698  * @hint: (nullable): an optional hint about what addin to use
1699  * @cancellable: (nullable): a #GCancellable
1700  * @callback: a #GAsyncReadyCallback to execute upon completion
1701  * @user_data: closure data for @callback
1702  *
1703  * Requests that the workbench open all of the #GFile denoted by @files.
1704  *
1705  * If @hint is provided, that will be used to determine what workbench
1706  * addin to use when opening the file. The @hint name should match the
1707  * module name of the plugin.
1708  *
1709  * Call ide_workbench_open_finish() from @callback to complete this
1710  * operation.
1711  *
1712  * Since: 3.32
1713  */
1714 void
ide_workbench_open_all_async(IdeWorkbench * self,GFile ** files,guint n_files,const gchar * hint,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1715 ide_workbench_open_all_async (IdeWorkbench         *self,
1716                               GFile               **files,
1717                               guint                 n_files,
1718                               const gchar          *hint,
1719                               GCancellable         *cancellable,
1720                               GAsyncReadyCallback   callback,
1721                               gpointer              user_data)
1722 {
1723   g_autoptr(IdeTask) task = NULL;
1724   g_autoptr(GPtrArray) ar = NULL;
1725   gint *n_active;
1726 
1727   g_return_if_fail (IDE_IS_WORKBENCH (self));
1728   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
1729 
1730   task = ide_task_new (self, cancellable, callback, user_data);
1731   ide_task_set_source_tag (task, ide_workbench_open_all_async);
1732 
1733   if (n_files == 0)
1734     {
1735       ide_task_return_boolean (task, TRUE);
1736       return;
1737     }
1738 
1739   ar = g_ptr_array_new_full (n_files, g_object_unref);
1740   for (guint i = 0; i < n_files; i++)
1741     g_ptr_array_add (ar, g_object_ref (files[i]));
1742 
1743   n_active = g_new0 (gint, 1);
1744   *n_active = ar->len;
1745   ide_task_set_task_data (task, n_active, g_free);
1746 
1747   for (guint i = 0; i < ar->len; i++)
1748     {
1749       GFile *file = g_ptr_array_index (ar, i);
1750 
1751       ide_workbench_open_async (self,
1752                                 file,
1753                                 hint,
1754                                 IDE_BUFFER_OPEN_FLAGS_NONE,
1755                                 cancellable,
1756                                 ide_workbench_open_all_cb,
1757                                 g_object_ref (task));
1758     }
1759 }
1760 
1761 /**
1762  * ide_workbench_open_async:
1763  * @self: an #IdeWorkbench
1764  * @file: a #GFile
1765  * @hint: (nullable): an optional hint about what addin to use
1766  * @flags: optional flags when opening the file
1767  * @cancellable: (nullable): a #GCancellable
1768  * @callback: a #GAsyncReadyCallback to execute upon completion
1769  * @user_data: closure data for @callback
1770  *
1771  * Requests that the workbench open @file.
1772  *
1773  * If @hint is provided, that will be used to determine what workbench
1774  * addin to use when opening the file. The @hint name should match the
1775  * module name of the plugin.
1776  *
1777  * @flags may be ignored by some backends.
1778  *
1779  * Since: 3.32
1780  */
1781 void
ide_workbench_open_async(IdeWorkbench * self,GFile * file,const gchar * hint,IdeBufferOpenFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1782 ide_workbench_open_async (IdeWorkbench        *self,
1783                           GFile               *file,
1784                           const gchar         *hint,
1785                           IdeBufferOpenFlags   flags,
1786                           GCancellable        *cancellable,
1787                           GAsyncReadyCallback  callback,
1788                           gpointer             user_data)
1789 {
1790   g_return_if_fail (IDE_IS_WORKBENCH (self));
1791   g_return_if_fail (G_IS_FILE (file));
1792   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
1793 
1794   ide_workbench_open_at_async (self,
1795                                file,
1796                                hint,
1797                                -1,
1798                                -1,
1799                                flags,
1800                                cancellable,
1801                                callback,
1802                                user_data);
1803 }
1804 
1805 static void
ide_workbench_open_cb(GObject * object,GAsyncResult * result,gpointer user_data)1806 ide_workbench_open_cb (GObject      *object,
1807                        GAsyncResult *result,
1808                        gpointer      user_data)
1809 {
1810   IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
1811   IdeWorkbenchAddin *next;
1812   g_autoptr(IdeTask) task = user_data;
1813   g_autoptr(GError) error = NULL;
1814   GCancellable *cancellable;
1815   Open *o;
1816 
1817   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
1818   g_assert (G_IS_ASYNC_RESULT (result));
1819   g_assert (IDE_IS_TASK (task));
1820 
1821   cancellable = ide_task_get_cancellable (task);
1822   o = ide_task_get_task_data (task);
1823 
1824   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
1825   g_assert (o != NULL);
1826   g_assert (o->addins != NULL);
1827   g_assert (o->addins->len > 0);
1828 
1829   if (ide_workbench_addin_open_finish (addin, result, &error))
1830     {
1831       ide_task_return_boolean (task, TRUE);
1832       return;
1833     }
1834 
1835   g_debug ("%s did not open the file, trying next.",
1836            G_OBJECT_TYPE_NAME (addin));
1837 
1838   g_ptr_array_remove (o->addins, addin);
1839 
1840   /*
1841    * We failed to open the file, try the next addin that is
1842    * left which said it supported the content-type.
1843    */
1844 
1845   if (o->addins->len == 0)
1846     {
1847       ide_task_return_new_error (task,
1848                                  G_IO_ERROR,
1849                                  G_IO_ERROR_FAILED,
1850                                  "Failed to locate addin supporting file");
1851       return;
1852     }
1853 
1854   next = g_ptr_array_index (o->addins, 0);
1855 
1856   ide_workbench_addin_open_at_async (next,
1857                                      o->file,
1858                                      o->content_type,
1859                                      o->at_line,
1860                                      o->at_line_offset,
1861                                      o->flags,
1862                                      cancellable,
1863                                      ide_workbench_open_cb,
1864                                      g_steal_pointer (&task));
1865 }
1866 
1867 static gint
sort_by_priority(gconstpointer a,gconstpointer b,gpointer user_data)1868 sort_by_priority (gconstpointer a,
1869                   gconstpointer b,
1870                   gpointer      user_data)
1871 {
1872   IdeWorkbenchAddin *addin_a = *(IdeWorkbenchAddin **)a;
1873   IdeWorkbenchAddin *addin_b = *(IdeWorkbenchAddin **)b;
1874   Open *o = user_data;
1875   gint prio_a = 0;
1876   gint prio_b = 0;
1877 
1878   if (!ide_workbench_addin_can_open (addin_a, o->file, o->content_type, &prio_a))
1879     return 1;
1880 
1881   if (!ide_workbench_addin_can_open (addin_b, o->file, o->content_type, &prio_b))
1882     return -1;
1883 
1884   if (prio_a < prio_b)
1885     return -1;
1886   else if (prio_a > prio_b)
1887     return 1;
1888   else
1889     return 0;
1890 }
1891 
1892 static void
ide_workbench_open_query_info_cb(GObject * object,GAsyncResult * result,gpointer user_data)1893 ide_workbench_open_query_info_cb (GObject      *object,
1894                                   GAsyncResult *result,
1895                                   gpointer      user_data)
1896 {
1897   GFile *file = (GFile *)object;
1898   g_autoptr(IdeTask) task = user_data;
1899   g_autoptr(GFileInfo) info = NULL;
1900   g_autoptr(GError) error = NULL;
1901   IdeWorkbenchAddin *first;
1902   GCancellable *cancellable;
1903   Open *o;
1904 
1905   g_assert (G_IS_FILE (file));
1906   g_assert (G_IS_ASYNC_RESULT (result));
1907   g_assert (IDE_IS_TASK (task));
1908 
1909   cancellable = ide_task_get_cancellable (task);
1910   o = ide_task_get_task_data (task);
1911 
1912   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
1913   g_assert (o != NULL);
1914   g_assert (o->addins != NULL);
1915   g_assert (o->addins->len > 0);
1916 
1917   if ((info = g_file_query_info_finish (file, result, &error)))
1918     o->content_type = g_strdup (g_file_info_get_content_type (info));
1919 
1920   /* Remove unsupported addins while iterating backwards so that
1921    * we can preserve the ordering of the array as we go.
1922    */
1923   for (guint i = o->addins->len; i > 0; i--)
1924     {
1925       IdeWorkbenchAddin *addin = g_ptr_array_index (o->addins, i - 1);
1926       gint prio = G_MAXINT;
1927 
1928       if (!ide_workbench_addin_can_open (addin, o->file, o->content_type, &prio))
1929         {
1930           g_ptr_array_remove_index_fast (o->addins, i - 1);
1931           if (o->preferred == addin)
1932             g_clear_object (&o->preferred);
1933         }
1934     }
1935 
1936   if (o->addins->len == 0)
1937     {
1938       ide_task_return_new_error (task,
1939                                  G_IO_ERROR,
1940                                  G_IO_ERROR_FAILED,
1941                                  "No addins can open the file");
1942       return;
1943     }
1944 
1945   /*
1946    * Now sort the addins by priority, so that we can attempt to load them
1947    * in the preferred ordering.
1948    */
1949   g_ptr_array_sort_with_data (o->addins, sort_by_priority, o);
1950 
1951   /*
1952    * Ensure that we place the preferred at the head of the array, so
1953    * that it gets preference over default priorities.
1954    */
1955   if (o->preferred != NULL)
1956     {
1957       g_ptr_array_insert (o->addins, 0, g_object_ref (o->preferred));
1958 
1959       for (guint i = 1; i < o->addins->len; i++)
1960         {
1961           if (g_ptr_array_index (o->addins, i) == (gpointer)o->preferred)
1962             {
1963               g_ptr_array_remove_index (o->addins, i);
1964               break;
1965             }
1966         }
1967     }
1968 
1969   /* Now start requesting that addins attempt to load the file. */
1970 
1971   first = g_ptr_array_index (o->addins, 0);
1972 
1973   ide_workbench_addin_open_at_async (first,
1974                                      o->file,
1975                                      o->content_type,
1976                                      o->at_line,
1977                                      o->at_line_offset,
1978                                      o->flags,
1979                                      cancellable,
1980                                      ide_workbench_open_cb,
1981                                      g_steal_pointer (&task));
1982 }
1983 
1984 /**
1985  * ide_workbench_open_at_async:
1986  * @self: an #IdeWorkbench
1987  * @file: a #GFile
1988  * @hint: (nullable): an optional hint about what addin to use
1989  * @at_line: the line number to open at, or -1 to ignore
1990  * @at_line_offset: the line offset to open at, or -1 to ignore
1991  * @flags: optional #IdeBufferOpenFlags
1992  * @cancellable: (nullable): a #GCancellable
1993  * @callback: a #GAsyncReadyCallback to execute upon completion
1994  * @user_data: closure data for @callback
1995  *
1996  * Like ide_workbench_open_async(), this allows opening a file
1997  * within the workbench. However, it also allows specifying a
1998  * line and column offset within the file to focus. Usually, this
1999  * only makes sense for files that can be opened in an editor.
2000  *
2001  * @at_line and @at_line_offset may be < 0 to ignore the parameters.
2002  *
2003  * @flags may be ignored by some backends
2004  *
2005  * Use ide_workbench_open_finish() to receive teh result of this
2006  * asynchronous operation.
2007  *
2008  * Since: 3.32
2009  */
2010 void
ide_workbench_open_at_async(IdeWorkbench * self,GFile * file,const gchar * hint,gint at_line,gint at_line_offset,IdeBufferOpenFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)2011 ide_workbench_open_at_async (IdeWorkbench        *self,
2012                              GFile               *file,
2013                              const gchar         *hint,
2014                              gint                 at_line,
2015                              gint                 at_line_offset,
2016                              IdeBufferOpenFlags   flags,
2017                              GCancellable        *cancellable,
2018                              GAsyncReadyCallback  callback,
2019                              gpointer             user_data)
2020 {
2021   g_autoptr(IdeTask) task = NULL;
2022   g_autoptr(GPtrArray) addins = NULL;
2023   IdeWorkbench *other;
2024   Open *o;
2025 
2026   g_return_if_fail (IDE_IS_WORKBENCH (self));
2027   g_return_if_fail (G_IS_FILE (file));
2028   g_return_if_fail (self->unloaded == FALSE);
2029   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
2030 
2031   /* Possibly re-route opening the file to another workbench if we
2032    * discover the file is a better fit over there.
2033    */
2034   other = ide_application_find_workbench_for_file (IDE_APPLICATION_DEFAULT, file);
2035 
2036   if (other != NULL && other != self)
2037     {
2038       ide_workbench_open_at_async (other,
2039                                    file,
2040                                    hint,
2041                                    at_line,
2042                                    at_line_offset,
2043                                    flags,
2044                                    cancellable,
2045                                    callback,
2046                                    user_data);
2047       return;
2048     }
2049 
2050   /* Canonicalize parameters */
2051   if (at_line < 0)
2052     at_line = -1;
2053   if (at_line_offset < 0)
2054     at_line_offset = -1;
2055 
2056   task = ide_task_new (self, cancellable, callback, user_data);
2057   ide_task_set_source_tag (task, ide_workbench_open_at_async);
2058 
2059   /*
2060    * Make sure we might have an addin to load after discovering
2061    * the files content-type.
2062    */
2063   if (!(addins = ide_workbench_collect_addins (self)) || addins->len == 0)
2064     {
2065       ide_task_return_new_error (task,
2066                                  G_IO_ERROR,
2067                                  G_IO_ERROR_FAILED,
2068                                  "No addins could open the file");
2069       return;
2070     }
2071 
2072   o = g_slice_new0 (Open);
2073   o->addins = g_ptr_array_ref (addins);
2074   if (hint != NULL)
2075     o->preferred = ide_workbench_find_addin (self, hint);
2076   o->file = g_object_ref (file);
2077   o->hint = g_strdup (hint);
2078   o->flags = flags;
2079   o->at_line = at_line;
2080   o->at_line_offset = at_line_offset;
2081   ide_task_set_task_data (task, o, open_free);
2082 
2083   g_file_query_info_async (file,
2084                            G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
2085                            G_FILE_QUERY_INFO_NONE,
2086                            G_PRIORITY_DEFAULT,
2087                            cancellable,
2088                            ide_workbench_open_query_info_cb,
2089                            g_steal_pointer (&task));
2090 }
2091 
2092 /**
2093  * ide_workbench_open_finish:
2094  * @self: an #IdeWorkbench
2095  * @result: a #GAsyncResult provided to callback
2096  * @error: a location for a #GError, or %NULL
2097  *
2098  * Completes a request to open a file using either
2099  * ide_workbench_open_async() or ide_workbench_open_at_async().
2100  *
2101  * Returns: %TRUE if the file was successfully opened; otherwise
2102  *   %FALSE and @error is set.
2103  *
2104  * Since: 3.32
2105  */
2106 gboolean
ide_workbench_open_finish(IdeWorkbench * self,GAsyncResult * result,GError ** error)2107 ide_workbench_open_finish (IdeWorkbench  *self,
2108                            GAsyncResult  *result,
2109                            GError       **error)
2110 {
2111   g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
2112   g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
2113 
2114   return ide_task_propagate_boolean (IDE_TASK (result), error);
2115 }
2116 
2117 /**
2118  * ide_workbench_get_current_workspace:
2119  * @self: a #IdeWorkbench
2120  *
2121  * Gets the most recently focused workspace, which may be used to
2122  * deliver events such as opening new pages.
2123  *
2124  * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
2125  *
2126  * Since: 3.32
2127  */
2128 IdeWorkspace *
ide_workbench_get_current_workspace(IdeWorkbench * self)2129 ide_workbench_get_current_workspace (IdeWorkbench *self)
2130 {
2131   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
2132 
2133   if (self->mru_queue.length > 0)
2134     return IDE_WORKSPACE (self->mru_queue.head->data);
2135 
2136   return NULL;
2137 }
2138 
2139 /**
2140  * ide_workbench_activate:
2141  * @self: a #IdeWorkbench
2142  *
2143  * This function will attempt to raise the most recently focused workspace.
2144  *
2145  * Since: 3.32
2146  */
2147 void
ide_workbench_activate(IdeWorkbench * self)2148 ide_workbench_activate (IdeWorkbench *self)
2149 {
2150   IdeWorkspace *workspace;
2151 
2152   g_return_if_fail (IDE_IS_WORKBENCH (self));
2153 
2154   if ((workspace = ide_workbench_get_current_workspace (self)))
2155     ide_workbench_focus_workspace (self, workspace);
2156 }
2157 
2158 static void
ide_workbench_propagate_vcs_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)2159 ide_workbench_propagate_vcs_cb (PeasExtensionSet *set,
2160                                 PeasPluginInfo   *plugin_info,
2161                                 PeasExtension    *exten,
2162                                 gpointer          user_data)
2163 {
2164   IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
2165   IdeVcs *vcs = user_data;
2166 
2167   g_assert (PEAS_IS_EXTENSION_SET (set));
2168   g_assert (plugin_info != NULL);
2169   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
2170   g_assert (!vcs || IDE_IS_VCS (vcs));
2171 
2172   ide_workbench_addin_vcs_changed (addin, vcs);
2173 }
2174 
2175 /**
2176  * ide_workbench_get_vcs:
2177  * @self: a #IdeWorkbench
2178  *
2179  * Gets the #IdeVcs that has been loaded for the workbench, if any.
2180  *
2181  * Returns: (transfer none) (nullable): an #IdeVcs or %NULL
2182  *
2183  * Since: 3.32
2184  */
2185 IdeVcs *
ide_workbench_get_vcs(IdeWorkbench * self)2186 ide_workbench_get_vcs (IdeWorkbench *self)
2187 {
2188   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
2189 
2190   return self->vcs;
2191 }
2192 
2193 /**
2194  * ide_workbench_get_vcs_monitor:
2195  * @self: a #IdeWorkbench
2196  *
2197  * Gets the #IdeVcsMonitor for the workbench, if any.
2198  *
2199  * Returns: (transfer none) (nullable): an #IdeVcsMonitor or %NULL
2200  *
2201  * Since: 3.32
2202  */
2203 IdeVcsMonitor *
ide_workbench_get_vcs_monitor(IdeWorkbench * self)2204 ide_workbench_get_vcs_monitor (IdeWorkbench *self)
2205 {
2206   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
2207 
2208   return self->vcs_monitor;
2209 }
2210 
2211 static void
remove_non_matching_vcs_cb(IdeObject * child,IdeVcs * vcs)2212 remove_non_matching_vcs_cb (IdeObject *child,
2213                             IdeVcs    *vcs)
2214 {
2215   g_assert (IDE_IS_MAIN_THREAD ());
2216   g_assert (IDE_IS_OBJECT (child));
2217   g_assert (IDE_IS_VCS (vcs));
2218 
2219   if (IDE_IS_VCS (child) && IDE_VCS (child) != vcs)
2220     ide_object_destroy (child);
2221 }
2222 
2223 static void
ide_workbench_vcs_notify_branch_name_cb(IdeWorkbench * self,GParamSpec * pspec,IdeVcs * vcs)2224 ide_workbench_vcs_notify_branch_name_cb (IdeWorkbench *self,
2225                                          GParamSpec   *pspec,
2226                                          IdeVcs       *vcs)
2227 {
2228   IdeBuildManager *build_manager;
2229 
2230   IDE_ENTRY;
2231 
2232   g_assert (IDE_IS_WORKBENCH (self));
2233   g_assert (IDE_IS_VCS (vcs));
2234 
2235   if (!ide_workbench_has_project (self))
2236     IDE_EXIT;
2237 
2238   build_manager = ide_build_manager_from_context (self->context);
2239   ide_build_manager_invalidate (build_manager);
2240 
2241   IDE_EXIT;
2242 }
2243 
2244 /**
2245  * ide_workbench_set_vcs:
2246  * @self: a #IdeWorkbench
2247  * @vcs: (nullable): an #IdeVcs
2248  *
2249  * Sets the #IdeVcs for the workbench.
2250  *
2251  * Since: 3.32
2252  */
2253 void
ide_workbench_set_vcs(IdeWorkbench * self,IdeVcs * vcs)2254 ide_workbench_set_vcs (IdeWorkbench *self,
2255                        IdeVcs       *vcs)
2256 {
2257   g_autoptr(IdeVcs) local_vcs = NULL;
2258   g_autoptr(GFile) local_workdir = NULL;
2259   g_autoptr(GFile) workdir = NULL;
2260 
2261   g_return_if_fail (IDE_IS_MAIN_THREAD ());
2262   g_return_if_fail (IDE_IS_WORKBENCH (self));
2263   g_return_if_fail (!vcs || IDE_IS_VCS (vcs));
2264 
2265   if (self->vcs != NULL && vcs == self->vcs)
2266     return;
2267 
2268   if (vcs == NULL)
2269     {
2270       local_workdir = ide_context_ref_workdir (self->context);
2271       vcs = local_vcs = IDE_VCS (ide_directory_vcs_new (local_workdir));
2272     }
2273 
2274   g_set_object (&self->vcs, vcs);
2275   ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (vcs));
2276   ide_object_foreach (IDE_OBJECT (self->context),
2277                       (GFunc)remove_non_matching_vcs_cb,
2278                       vcs);
2279 
2280   if ((workdir = ide_vcs_get_workdir (vcs)))
2281     ide_context_set_workdir (self->context, workdir);
2282 
2283   ide_vcs_monitor_set_vcs (self->vcs_monitor, self->vcs);
2284 
2285   peas_extension_set_foreach (self->addins,
2286                               ide_workbench_propagate_vcs_cb,
2287                               self->vcs);
2288 
2289   g_signal_connect_object (vcs,
2290                            "notify::branch-name",
2291                            G_CALLBACK (ide_workbench_vcs_notify_branch_name_cb),
2292                            self,
2293                            G_CONNECT_SWAPPED);
2294 
2295   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VCS]);
2296 }
2297 
2298 /**
2299  * ide_workbench_get_build_system:
2300  * @self: a #IdeWorkbench
2301  *
2302  * Gets the #IdeBuildSystem for the workbench, if any.
2303  *
2304  * Returns: (transfer none) (nullable): an #IdeBuildSystem or %NULL
2305  *
2306  * Since: 3.32
2307  */
2308 IdeBuildSystem *
ide_workbench_get_build_system(IdeWorkbench * self)2309 ide_workbench_get_build_system (IdeWorkbench *self)
2310 {
2311   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
2312   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
2313 
2314   return self->build_system;
2315 }
2316 
2317 static void
remove_non_matching_build_systems_cb(IdeObject * child,IdeBuildSystem * build_system)2318 remove_non_matching_build_systems_cb (IdeObject      *child,
2319                                       IdeBuildSystem *build_system)
2320 {
2321   g_assert (IDE_IS_MAIN_THREAD ());
2322   g_assert (IDE_IS_OBJECT (child));
2323   g_assert (IDE_IS_BUILD_SYSTEM (build_system));
2324 
2325   if (IDE_IS_BUILD_SYSTEM (child) && IDE_BUILD_SYSTEM (child) != build_system)
2326     ide_object_destroy (child);
2327 }
2328 
2329 /**
2330  * ide_workbench_set_build_system:
2331  * @self: a #IdeWorkbench
2332  * @build_system: (nullable): an #IdeBuildSystem or %NULL
2333  *
2334  * Sets the #IdeBuildSystem for the workbench.
2335  *
2336  * If @build_system is %NULL, then a fallback build system will be used
2337  * instead. It does not provide building capabilities, but allows for some
2338  * components that require a build system to continue functioning.
2339  *
2340  * Since: 3.32
2341  */
2342 void
ide_workbench_set_build_system(IdeWorkbench * self,IdeBuildSystem * build_system)2343 ide_workbench_set_build_system (IdeWorkbench   *self,
2344                                 IdeBuildSystem *build_system)
2345 {
2346   g_autoptr(IdeBuildSystem) local_build_system = NULL;
2347   IdeBuildManager *build_manager;
2348 
2349   g_return_if_fail (IDE_IS_WORKBENCH (self));
2350   g_return_if_fail (!build_system || IDE_IS_BUILD_SYSTEM (build_system));
2351 
2352   if (build_system == self->build_system)
2353     return;
2354 
2355   /* We want there to always be a build system available so that various
2356    * plugins don't need lots of extra code to handle the %NULL case. So
2357    * if @build_system is %NULL, then we'll create a fallback build system
2358    * and assign that instead.
2359    */
2360 
2361   if (build_system == NULL)
2362     build_system = local_build_system = ide_fallback_build_system_new ();
2363 
2364   /* We want to add our new build system before removing the old build
2365    * system to ensure there is always an #IdeBuildSystem child of the
2366    * IdeContext.
2367    */
2368   g_set_object (&self->build_system, build_system);
2369   ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (build_system));
2370 
2371   /* Now remove any previous build-system from the context */
2372   ide_object_foreach (IDE_OBJECT (self->context),
2373                       (GFunc)remove_non_matching_build_systems_cb,
2374                       build_system);
2375 
2376   /* Ask the build-manager to setup a new pipeline */
2377   if ((build_manager = ide_context_peek_child_typed (self->context, IDE_TYPE_BUILD_MANAGER)))
2378     ide_build_manager_invalidate (build_manager);
2379 }
2380 
2381 /**
2382  * ide_workbench_get_workspace_by_type:
2383  * @self: a #IdeWorkbench
2384  * @type: a #GType of a subclass of #IdeWorkspace
2385  *
2386  * Gets the most-recently-used workspace that matches @type.
2387  *
2388  * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
2389  *
2390  * Since: 3.32
2391  */
2392 IdeWorkspace *
ide_workbench_get_workspace_by_type(IdeWorkbench * self,GType type)2393 ide_workbench_get_workspace_by_type (IdeWorkbench *self,
2394                                      GType         type)
2395 {
2396   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
2397   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
2398   g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_WORKSPACE), NULL);
2399 
2400   for (const GList *iter = self->mru_queue.head; iter; iter = iter->next)
2401     {
2402       if (G_TYPE_CHECK_INSTANCE_TYPE (iter->data, type))
2403         return IDE_WORKSPACE (iter->data);
2404     }
2405 
2406   return NULL;
2407 }
2408 
2409 gboolean
_ide_workbench_is_last_workspace(IdeWorkbench * self,IdeWorkspace * workspace)2410 _ide_workbench_is_last_workspace (IdeWorkbench *self,
2411                                   IdeWorkspace *workspace)
2412 {
2413   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
2414   g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
2415 
2416   return self->mru_queue.length == 1 &&
2417          g_queue_peek_head (&self->mru_queue) == (gpointer)workspace;
2418 }
2419 
2420 /**
2421  * ide_workbench_has_project:
2422  * @self: a #IdeWorkbench
2423  *
2424  * Returns %TRUE if a project is loaded (or currently loading) in the
2425  * workbench.
2426  *
2427  * Returns: %TRUE if the workbench has a project
2428  *
2429  * Since: 3.32
2430  */
2431 gboolean
ide_workbench_has_project(IdeWorkbench * self)2432 ide_workbench_has_project (IdeWorkbench *self)
2433 {
2434   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
2435   g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
2436 
2437   return self->project_info != NULL;
2438 }
2439 
2440 /**
2441  * ide_workbench_addin_find_by_module_name:
2442  * @workbench: an #IdeWorkbench
2443  * @module_name: the name of the addin module
2444  *
2445  * Finds the addin (if any) matching the plugin's @module_name.
2446  *
2447  * Returns: (transfer none) (nullable): an #IdeWorkbenchAddin or %NULL
2448  *
2449  * Since: 3.32
2450  */
2451 IdeWorkbenchAddin *
ide_workbench_addin_find_by_module_name(IdeWorkbench * workbench,const gchar * module_name)2452 ide_workbench_addin_find_by_module_name (IdeWorkbench *workbench,
2453                                          const gchar  *module_name)
2454 {
2455   PeasPluginInfo *plugin_info;
2456   PeasExtension *ret = NULL;
2457   PeasEngine *engine;
2458 
2459   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
2460   g_return_val_if_fail (IDE_IS_WORKBENCH (workbench), NULL);
2461   g_return_val_if_fail (module_name != NULL, NULL);
2462 
2463   if (workbench->addins == NULL)
2464     return NULL;
2465 
2466   engine = peas_engine_get_default ();
2467 
2468   if ((plugin_info = peas_engine_get_plugin_info (engine, module_name)))
2469     ret = peas_extension_set_get_extension (workbench->addins, plugin_info);
2470 
2471   return IDE_WORKBENCH_ADDIN (ret);
2472 }
2473 
2474 static void
ide_workbench_resolve_file_worker(IdeTask * task,gpointer source_object,gpointer task_data,GCancellable * cancellable)2475 ide_workbench_resolve_file_worker (IdeTask      *task,
2476                                    gpointer      source_object,
2477                                    gpointer      task_data,
2478                                    GCancellable *cancellable)
2479 {
2480   ResolveFile *rf = task_data;
2481   g_autofree gchar *basename = NULL;
2482 
2483   g_assert (IDE_IS_TASK (task));
2484   g_assert (IDE_IS_WORKBENCH (source_object));
2485   g_assert (rf != NULL);
2486   g_assert (rf->roots != NULL);
2487   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
2488 
2489   basename = g_path_get_basename (rf->path);
2490 
2491   for (guint i = 0; i < rf->roots->len; i++)
2492     {
2493       GFile *root = g_ptr_array_index (rf->roots, i);
2494       g_autoptr(GFile) child = g_file_get_child (root, rf->path);
2495       g_autoptr(GPtrArray) found = NULL;
2496 
2497       if (g_file_query_exists (child, cancellable))
2498         {
2499           ide_task_return_pointer (task, g_steal_pointer (&child), g_object_unref);
2500           return;
2501         }
2502 
2503       found = ide_g_file_find_with_depth (root, basename, 0, cancellable);
2504       IDE_PTR_ARRAY_SET_FREE_FUNC (found, g_object_unref);
2505 
2506       if (found != NULL && found->len > 0)
2507         {
2508           GFile *match = g_ptr_array_index (found, 0);
2509           ide_task_return_pointer (task, g_object_ref (match), g_object_unref);
2510           return;
2511         }
2512     }
2513 
2514   ide_task_return_new_error (task,
2515                              G_IO_ERROR,
2516                              G_IO_ERROR_NOT_FOUND,
2517                              "Failed to locate file %s",
2518                              basename);
2519 }
2520 
2521 /**
2522  * ide_workbench_resolve_file_async:
2523  * @self: a #IdeWorkbench
2524  * @filename: the filename to discover
2525  *
2526  * This function will try to locate a given file based on the filename,
2527  * possibly resolving it from a build directory, or source directory.
2528  *
2529  * If no file was discovered, some attempt will be made to locate a file
2530  * that matches appropriately.
2531  *
2532  * Since: 3.32
2533  */
2534 void
ide_workbench_resolve_file_async(IdeWorkbench * self,const gchar * filename,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)2535 ide_workbench_resolve_file_async (IdeWorkbench        *self,
2536                                   const gchar         *filename,
2537                                   GCancellable        *cancellable,
2538                                   GAsyncReadyCallback  callback,
2539                                   gpointer             user_data)
2540 {
2541   g_autoptr(IdeTask) task = NULL;
2542   ResolveFile *rf;
2543 
2544   IDE_ENTRY;
2545 
2546   g_return_if_fail (IDE_IS_MAIN_THREAD ());
2547   g_return_if_fail (IDE_IS_WORKBENCH (self));
2548   g_return_if_fail (filename != NULL);
2549 
2550   task = ide_task_new (self, cancellable, callback, user_data);
2551   ide_task_set_source_tag (task, ide_workbench_resolve_file_async);
2552 
2553   rf = g_slice_new0 (ResolveFile);
2554   rf->roots = g_ptr_array_new_with_free_func (g_object_unref);
2555   rf->path = g_strdup (filename);
2556 
2557   g_ptr_array_add (rf->roots, ide_context_ref_workdir (self->context));
2558 
2559   if (ide_workbench_has_project (self))
2560     {
2561       IdeBuildManager *build_manager = ide_build_manager_from_context (self->context);
2562       IdePipeline *pipeline = ide_build_manager_get_pipeline (build_manager);
2563 
2564       if (pipeline != NULL)
2565         {
2566           const gchar *builddir = ide_pipeline_get_builddir (pipeline);
2567 
2568           g_ptr_array_add (rf->roots, g_file_new_for_path (builddir));
2569         }
2570     }
2571 
2572   ide_task_set_task_data (task, rf, resolve_file_free);
2573   ide_task_run_in_thread (task, ide_workbench_resolve_file_worker);
2574 
2575   IDE_EXIT;
2576 }
2577 
2578 /**
2579  * ide_workbench_resolve_file_finish:
2580  * @self: a #IdeWorkbench
2581  * @result: a #GAsyncResult
2582  * @error: a location for a #GError
2583  *
2584  * Completes an asynchronous request to ide_workbench_resolve_file_async().
2585  *
2586  * Returns: (transfer full): a #GFile, or %NULL and @error is set
2587  *
2588  * Since: 3.32
2589  */
2590 GFile *
ide_workbench_resolve_file_finish(IdeWorkbench * self,GAsyncResult * result,GError ** error)2591 ide_workbench_resolve_file_finish (IdeWorkbench  *self,
2592                                    GAsyncResult  *result,
2593                                    GError       **error)
2594 {
2595   GFile *ret;
2596 
2597   IDE_ENTRY;
2598 
2599   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
2600   g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
2601   g_return_val_if_fail (IDE_IS_TASK (result), NULL);
2602 
2603   ret = ide_task_propagate_pointer (IDE_TASK (result), error);
2604 
2605   IDE_RETURN (g_steal_pointer (&ret));
2606 }
2607