diff --git a/gplugin/gplugin-file-source.h b/gplugin/gplugin-file-source.h
new file mode 100644
--- /dev/null
+++ b/gplugin/gplugin-file-source.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2011-2022 Gary Kramlich <grim@reaperworld.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef GPLUGIN_FILE_SOURCE_H
+#define GPLUGIN_FILE_SOURCE_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gplugin/gplugin-manager.h>
+#include <gplugin/gplugin-source.h>
+
+G_BEGIN_DECLS
+
+#define GPLUGIN_TYPE_FILE_SOURCE (gplugin_file_source_get_type())
+G_DECLARE_FINAL_TYPE(
+	GPluginFileSource,
+	gplugin_file_source,
+	GPLUGIN,
+	FILE_SOURCE,
+	GObject)
+
+GPluginSource *gplugin_file_source_new(GPluginManager *manager);
+
+G_END_DECLS
+
+#endif /* GPLUGIN_FILE_SOURCE_H */
# HG changeset patch
# Node ID 1bcf543dfe08
# Parent  741b8a475cb7
diff --git a/gplugin/gplugin-file-source.c b/gplugin/gplugin-file-source.c
new file mode 100644
--- /dev/null
+++ b/gplugin/gplugin-file-source.c
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2011-2022 Gary Kramlich <grim@reaperworld.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <gplugin/gplugin-file-source.h>
+
+#include <gplugin/gplugin-file-tree.h>
+#include <gplugin/gplugin-private.h>
+
+/**
+ * GPluginFileSource:
+ *
+ * A [iface@GPlugin.Source] that will query plugins on disk.
+ *
+ * Since: 0.39.0
+ */
+
+struct _GPluginFileSource {
+	GObject parent;
+
+	GPluginManager *manager;
+
+	GHashTable *plugin_filenames;
+	GHashTable *loaders_by_extension;
+	GNode *root;
+
+	GList *error_messages;
+};
+
+enum {
+	PROP_ZERO,
+	PROP_MANAGER,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {
+	NULL,
+};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static guint
+gplugin_file_source_str_hash(gconstpointer v)
+{
+	if(v == NULL) {
+		return g_str_hash("");
+	}
+
+	return g_str_hash(v);
+}
+
+static void
+gplugin_file_source_set_manager(
+	GPluginFileSource *source,
+	GPluginManager *manager)
+{
+	if(g_set_object(&source->manager, manager)) {
+		g_object_notify_by_pspec(G_OBJECT(source), properties[PROP_MANAGER]);
+	}
+}
+
+static GPluginManager *
+gplugin_file_source_get_manager(GPluginFileSource *source)
+{
+	return source->manager;
+}
+
+static void
+gplugin_file_source_add_loader(GPluginFileSource *source, GPluginLoader *loader)
+{
+	GSList *exts = NULL;
+	const gchar *loader_id = NULL;
+
+	loader_id = gplugin_loader_get_id(loader);
+
+	exts = gplugin_loader_get_supported_extensions(loader);
+	while(exts != NULL) {
+		GSList *loaders = NULL;
+		const gchar *extension = exts->data;
+
+		/* Grab any existing loaders that are registered for this extension so
+		 * that we can prepend our loader. But before we add ours, we remove any
+		 * old copies we might have of ours.
+		 */
+		loaders = g_hash_table_lookup(source->loaders_by_extension, extension);
+		for(GSList *ll = loaders; ll != NULL; ll = ll->next) {
+			GPluginLoader *existing = ll->data;
+			const gchar *existing_id = gplugin_loader_get_id(existing);
+
+			if(g_str_equal(loader_id, existing_id)) {
+				loaders = g_slist_remove(loaders, existing);
+
+				g_clear_object(&existing);
+
+				break;
+			}
+		}
+
+		loaders = g_slist_prepend(loaders, g_object_ref(loader));
+
+		/* Now insert the updated slist back into the hash table */
+		g_hash_table_insert(
+			source->loaders_by_extension,
+			g_strdup(extension),
+			loaders);
+
+		/* Move exts to the next one. */
+		exts = g_slist_delete_link(exts, exts);
+	}
+}
+
+static void
+gplugin_file_source_remove_loader(
+	GPluginFileSource *source,
+	GPluginLoader *loader)
+{
+	GSList *exts = NULL;
+	const gchar *loader_id = NULL;
+
+	loader_id = gplugin_loader_get_id(loader);
+
+	exts = gplugin_loader_get_supported_extensions(loader);
+	while(exts != NULL) {
+		GSList *loaders = NULL;
+		const gchar *extension = exts->data;
+
+		loaders = g_hash_table_lookup(source->loaders_by_extension, extension);
+		for(GSList *ll = loaders; ll != NULL; ll = ll->next) {
+			GPluginLoader *existing = ll->data;
+			const gchar *existing_id = gplugin_loader_get_id(existing);
+
+			if(g_str_equal(loader_id, existing_id)) {
+				loaders = g_slist_remove(loaders, existing);
+
+				if(loaders == NULL) {
+					g_hash_table_remove(
+						source->loaders_by_extension,
+						extension);
+				} else {
+					g_hash_table_insert(
+						source->loaders_by_extension,
+						g_strdup(extension),
+						loaders);
+				}
+
+				g_clear_object(&existing);
+
+				break;
+			}
+		}
+
+		/* Move exts to the next one. */
+		exts = g_slist_delete_link(exts, exts);
+	}
+}
+
+static void
+gplugin_file_source_update_loaders(GPluginFileSource *source)
+{
+	GList *loaders = NULL;
+
+	loaders = gplugin_manager_get_loaders(source->manager);
+	while(loaders != NULL) {
+		GPluginLoader *loader = loaders->data;
+
+		gplugin_file_source_add_loader(source, loader);
+
+		loaders = g_list_delete_link(loaders, loaders);
+	}
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+gplugin_file_source_loader_registered_cb(
+	G_GNUC_UNUSED GPluginManager *manager,
+	GPluginLoader *loader,
+	gpointer data)
+{
+	gplugin_file_source_add_loader(data, loader);
+}
+
+static void
+gplugin_file_source_loader_unregistered_cb(
+	G_GNUC_UNUSED GPluginManager *manager,
+	GPluginLoader *loader,
+	gpointer data)
+{
+	gplugin_file_source_remove_loader(data, loader);
+}
+
+/******************************************************************************
+ * GPluginSource implementation
+ *****************************************************************************/
+static gboolean
+gplugin_file_source_scan(GPluginSource *source)
+{
+	GPluginFileSource *file_source = GPLUGIN_FILE_SOURCE(source);
+	gboolean refresh = FALSE;
+	gint errors = 0;
+
+	/* Clear any error messages from our last scan. */
+	g_list_free_full(file_source->error_messages, (GDestroyNotify)g_free);
+	file_source->error_messages = NULL;
+
+	for(GNode *dir = file_source->root->children; dir; dir = dir->next) {
+		GPluginFileTreeEntry *e = dir->data;
+		GNode *file = NULL;
+		const gchar *path = e->filename;
+
+		for(file = dir->children; file; file = file->next) {
+			GPluginPlugin *plugin = NULL;
+			GPluginLoader *loader = NULL;
+			GError *error = NULL;
+			GSList *l = NULL;
+			gchar *filename = NULL;
+
+			e = (GPluginFileTreeEntry *)file->data;
+
+			/* Build the path and see if we need to probe it! */
+			filename = g_build_filename(path, e->filename, NULL);
+			plugin =
+				g_hash_table_lookup(file_source->plugin_filenames, filename);
+
+			if(plugin && GPLUGIN_IS_PLUGIN(plugin)) {
+				GPluginPluginState state = gplugin_plugin_get_state(plugin);
+
+				/* The plugin is in our "view", check its state.  If it's
+				 * queried or loaded, move on to the next one.
+				 */
+				if(state == GPLUGIN_PLUGIN_STATE_QUERIED ||
+				   state == GPLUGIN_PLUGIN_STATE_LOADED) {
+					g_free(filename);
+					continue;
+				}
+			}
+
+			/* grab the list of loaders for this extension */
+			l = g_hash_table_lookup(
+				file_source->loaders_by_extension,
+				e->extension);
+			for(; l; l = l->next) {
+				if(!GPLUGIN_IS_LOADER(l->data)) {
+					continue;
+				}
+
+				loader = GPLUGIN_LOADER(l->data);
+
+				/* Try to probe the plugin with the current loader */
+				plugin = gplugin_loader_query_plugin(loader, filename, &error);
+
+				/* Check the GError, if it's set, output its message and
+				 * try the next loader.
+				 */
+				if(error) {
+					gchar *error_message = NULL;
+
+					errors++;
+
+					error_message = g_strdup_printf(
+						"failed to query '%s' with loader '%s': %s",
+						filename,
+						G_OBJECT_TYPE_NAME(loader),
+						error->message);
+
+					file_source->error_messages = g_list_prepend(
+						file_source->error_messages,
+						error_message);
+
+					g_clear_error(&error);
+					g_clear_object(&plugin);
+
+					loader = NULL;
+
+					continue;
+				}
+
+				/* if the plugin instance is good, then break out of this
+				 * loop.
+				 */
+				if(GPLUGIN_IS_PLUGIN(plugin)) {
+					break;
+				}
+
+				g_object_unref(G_OBJECT(plugin));
+
+				loader = NULL;
+			}
+
+			/* check if our plugin instance is good.  If it's not good we
+			 * don't need to do anything but free the filename which we'll
+			 * do later.
+			 */
+			if(GPLUGIN_IS_PLUGIN(plugin)) {
+				/* We have a good plugin, huzzah! We need to add it to our hash
+				 * table.
+				 */
+
+				gchar *real_filename = gplugin_plugin_get_filename(plugin);
+
+				/* We also need the GPluginPluginInfo for a bunch of stuff. */
+				GPluginPluginInfo *info = gplugin_plugin_get_info(plugin);
+
+				const gchar *id = gplugin_plugin_info_get_id(info);
+				GSList *l = NULL, *ll = NULL;
+				gboolean seen = FALSE;
+
+				/* Throw a warning if the info->id is NULL. */
+				if(id == NULL) {
+					gchar *error_message = NULL;
+
+					error_message = g_strdup_printf(
+						"plugin %s has a NULL id",
+						real_filename);
+
+					g_free(real_filename);
+					g_object_unref(G_OBJECT(info));
+
+					file_source->error_messages = g_list_prepend(
+						file_source->error_messages,
+						error_message);
+
+					continue;
+				}
+
+				/* Now insert into our hash table. */
+				g_hash_table_replace(
+					file_source->plugin_filenames,
+					real_filename,
+					g_object_ref(G_OBJECT(plugin)));
+
+				/* Grab the list of plugins with our id and prepend the new
+				 * plugin to it before updating it.
+				 */
+				l = gplugin_manager_find_plugins(file_source->manager, id);
+				for(ll = l; ll; ll = ll->next) {
+					GPluginPlugin *splugin = GPLUGIN_PLUGIN(ll->data);
+					gchar *sfilename = gplugin_plugin_get_filename(splugin);
+
+					if(!g_strcmp0(real_filename, sfilename)) {
+						seen = TRUE;
+					}
+
+					g_free(sfilename);
+				}
+				if(!seen) {
+					gplugin_manager_add_plugin(
+						file_source->manager,
+						id,
+						plugin);
+				}
+
+				/* Check if the plugin is supposed to be loaded on query, and
+				 * if so, load it.
+				 */
+				if(gplugin_plugin_info_get_load_on_query(info)) {
+					GError *error = NULL;
+					gboolean loaded;
+
+					loaded = gplugin_loader_load_plugin(loader, plugin, &error);
+
+					if(!loaded) {
+						gchar *error_message = NULL;
+
+						error_message = g_strdup_printf(
+							"failed to load %s during query: %s",
+							filename,
+							(error) ? error->message : "unknown");
+
+						file_source->error_messages = g_list_prepend(
+							file_source->error_messages,
+							error_message);
+
+						errors++;
+
+						g_clear_error(&error);
+					}
+				} else {
+					if(errors > 0) {
+						refresh = TRUE;
+					}
+				}
+
+				g_object_unref(G_OBJECT(info));
+
+				/* Since the plugin is now stored in our hash tables we need to
+				 * remove this function's reference to it.
+				 */
+				g_object_unref(G_OBJECT(plugin));
+			}
+
+			g_free(filename);
+		}
+	}
+
+	return refresh;
+}
+
+static void
+gplugin_file_source_source_iface_init(GPluginSourceInterface *iface)
+{
+	iface->scan = gplugin_file_source_scan;
+}
+
+/******************************************************************************
+ * GObject implementation
+ *****************************************************************************/
+G_DEFINE_TYPE_WITH_CODE(
+	GPluginFileSource,
+	gplugin_file_source,
+	G_TYPE_OBJECT,
+	G_IMPLEMENT_INTERFACE(
+		GPLUGIN_TYPE_SOURCE,
+		gplugin_file_source_source_iface_init))
+
+static void
+gplugin_file_source_get_property(
+	GObject *obj,
+	guint param_id,
+	GValue *value,
+	GParamSpec *pspec)
+{
+	GPluginFileSource *source = GPLUGIN_FILE_SOURCE(obj);
+
+	switch(param_id) {
+		case PROP_MANAGER:
+			g_value_set_object(value, gplugin_file_source_get_manager(source));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+gplugin_file_source_set_property(
+	GObject *obj,
+	guint param_id,
+	const GValue *value,
+	GParamSpec *pspec)
+{
+	GPluginFileSource *source = GPLUGIN_FILE_SOURCE(obj);
+
+	switch(param_id) {
+		case PROP_MANAGER:
+			gplugin_file_source_set_manager(source, g_value_get_object(value));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+	}
+}
+
+static void
+gplugin_file_source_dispose(GObject *obj)
+{
+	GPluginFileSource *source = GPLUGIN_FILE_SOURCE(obj);
+
+	g_clear_object(&source->manager);
+
+	G_OBJECT_CLASS(gplugin_file_source_parent_class)->dispose(obj);
+}
+
+static void
+gplugin_file_source_constructed(GObject *obj)
+{
+	GPluginFileSource *source = GPLUGIN_FILE_SOURCE(obj);
+	GList *paths = NULL;
+
+	G_OBJECT_CLASS(gplugin_file_source_parent_class)->constructed(obj);
+
+	/* The plugin_filenames hash table is keyed on the filename of the plugin
+	 * with a value of the plugin itself.
+	 */
+	source->plugin_filenames =
+		g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_object_unref);
+
+	/* The loaders_by_extension hash table is keyed on the supported extensions
+	 * of the loader.  Which means that a loader that supports multiple
+	 * extensions will be in the table multiple times.
+	 *
+	 * We deal with collisions by using a GSList for the value which will hold
+	 * references to instances of the actual loaders.
+	 *
+	 * Storing this in this method allows us to quickly figure out which loader
+	 * to use by the filename and helps us to avoid iterating the loaders table
+	 * again and again.
+	 */
+	source->loaders_by_extension = g_hash_table_new_full(
+		gplugin_file_source_str_hash,
+		g_str_equal,
+		g_free,
+		NULL);
+
+	/* Connect to the loader-registered and loader-unregistered signals so we
+	 * can keep our loaders_by_extension hash table up to date.
+	 */
+	g_signal_connect_object(
+		source->manager,
+		"loader-registered",
+		G_CALLBACK(gplugin_file_source_loader_registered_cb),
+		source,
+		0);
+	g_signal_connect_object(
+		source->manager,
+		"loader-unregistered",
+		G_CALLBACK(gplugin_file_source_loader_unregistered_cb),
+		source,
+		0);
+
+	gplugin_file_source_update_loaders(source);
+
+	/* Get the paths from the manager and create our initial file tree. */
+	paths = gplugin_manager_get_paths(source->manager);
+	source->root = gplugin_file_tree_new(paths);
+}
+
+static void
+gplugin_file_source_finalize(GObject *obj)
+{
+	GPluginFileSource *source = GPLUGIN_FILE_SOURCE(obj);
+
+	g_clear_pointer(&source->root, gplugin_file_tree_free);
+	g_clear_pointer(&source->plugin_filenames, g_hash_table_destroy);
+	g_clear_pointer(&source->loaders_by_extension, g_hash_table_destroy);
+
+	while(source->error_messages != NULL) {
+		gchar *error_message = source->error_messages->data;
+
+		g_warning("%s", error_message);
+
+		g_free(error_message);
+
+		source->error_messages =
+			g_list_delete_link(source->error_messages, source->error_messages);
+	}
+
+	G_OBJECT_CLASS(gplugin_file_source_parent_class)->finalize(obj);
+}
+
+static void
+gplugin_file_source_init(G_GNUC_UNUSED GPluginFileSource *source)
+{
+}
+
+static void
+gplugin_file_source_class_init(GPluginFileSourceClass *klass)
+{
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->get_property = gplugin_file_source_get_property;
+	obj_class->set_property = gplugin_file_source_set_property;
+	obj_class->constructed = gplugin_file_source_constructed;
+	obj_class->dispose = gplugin_file_source_dispose;
+	obj_class->finalize = gplugin_file_source_finalize;
+
+	/**
+	 * GPluginFileSource::manager:
+	 *
+	 * The [class@GPlugin.Manager] that this source is working for.
+	 *
+	 * Since: 0.39.0
+	 */
+	properties[PROP_MANAGER] = g_param_spec_object(
+		"manager",
+		"manager",
+		"The manager this source is working for.",
+		GPLUGIN_TYPE_MANAGER,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+
+/**
+ * gplugin_file_source_new:
+ * @manager: The [class@GPlugin.Manager] instance.
+ *
+ * Creates a [iface@GPlugin.Source] that will query plugins on disk using the
+ * paths from @manager.
+ *
+ * Returns: (transfer full): The new source.
+ *
+ * Since: 0.39.0
+ */
+GPluginSource *
+gplugin_file_source_new(GPluginManager *manager)
+{
+	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), NULL);
+
+	return g_object_new(GPLUGIN_TYPE_FILE_SOURCE, "manager", manager, NULL);
+}
diff --git a/gplugin/gplugin-manager.c b/gplugin/gplugin-manager.c
--- a/gplugin/gplugin-manager.c
+++ b/gplugin/gplugin-manager.c
@@ -22,10 +22,12 @@
 #include <glib/gi18n-lib.h>
 
 #include <gplugin/gplugin-core.h>
+#include <gplugin/gplugin-file-source.h>
 #include <gplugin/gplugin-file-tree.h>
 #include <gplugin/gplugin-manager.h>
 #include <gplugin/gplugin-native-loader.h>
 #include <gplugin/gplugin-private.h>
+#include <gplugin/gplugin-source.h>
 
 /**
  * GPluginManagerForeachFunc:
@@ -70,10 +72,8 @@
 
 	GQueue *paths;
 	GHashTable *plugins;
-	GHashTable *plugins_filename_view;
 
 	GHashTable *loaders;
-	GHashTable *loaders_by_extension;
 
 	gboolean refresh_needed;
 };
@@ -95,15 +95,6 @@
 /******************************************************************************
  * Helpers
  *****************************************************************************/
-static guint
-gplugin_manager_str_hash(gconstpointer v)
-{
-	if(v == NULL)
-		return g_str_hash("");
-
-	return g_str_hash(v);
-}
-
 static gboolean
 gplugin_manager_remove_list_value(
 	G_GNUC_UNUSED gpointer k,
@@ -290,19 +281,9 @@
 		NULL);
 	g_clear_pointer(&manager->plugins, g_hash_table_destroy);
 
-	/* destroy the filename view */
-	g_clear_pointer(&manager->plugins_filename_view, g_hash_table_destroy);
-
 	/* clean up our list of loaders */
 	g_clear_pointer(&manager->loaders, g_hash_table_destroy);
 
-	/* free all the data in the loaders hash table and destroy it */
-	g_hash_table_foreach_remove(
-		manager->loaders_by_extension,
-		gplugin_manager_remove_list_value,
-		NULL);
-	g_clear_pointer(&manager->loaders_by_extension, g_hash_table_destroy);
-
 	/* call the base class's destructor */
 	G_OBJECT_CLASS(gplugin_manager_parent_class)->finalize(obj);
 }
@@ -497,31 +478,8 @@
 	manager->plugins =
 		g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
 
-	/* the filename view is hash table keyed on the filename of the plugin with
-	 * a value of the plugin itself.
-	 */
-	manager->plugins_filename_view =
-		g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_object_unref);
-
 	manager->loaders =
 		g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_object_unref);
-
-	/* The loaders_by_extension hash table is keyed on the supported extensions
-	 * of the loader.  Which means that a loader that supports multiple
-	 * extensions will be in the table multiple times.
-	 *
-	 * We deal with collisions by using a GSList for the value which will hold
-	 * references to instances of the actual loaders.
-	 *
-	 * Storing this in this method allows us to quickly figure out which loader
-	 * to use by the filename and helps us to avoid iterating the loaders table
-	 * again and again.
-	 */
-	manager->loaders_by_extension = g_hash_table_new_full(
-		gplugin_manager_str_hash,
-		g_str_equal,
-		g_free,
-		NULL);
 }
 
 /******************************************************************************
@@ -806,7 +764,6 @@
 	GError **error)
 {
 	GPluginLoader *found = NULL;
-	GSList *l = NULL, *exts = NULL;
 	const gchar *id = NULL;
 
 	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), FALSE);
@@ -826,39 +783,6 @@
 
 	g_hash_table_insert(manager->loaders, g_strdup(id), g_object_ref(loader));
 
-	exts = gplugin_loader_get_supported_extensions(loader);
-	for(l = exts; l; l = l->next) {
-		GSList *existing = NULL, *ll = NULL;
-		const gchar *ext = (const gchar *)l->data;
-
-		/* grab any existing loaders that are registered for this type so that
-		 * we can prepend our loader.  But before we add ours, we remove any
-		 * old copies we might have of ours.
-		 */
-		existing = g_hash_table_lookup(manager->loaders_by_extension, ext);
-		for(ll = existing; ll; ll = ll->next) {
-			GPluginLoader *iter = GPLUGIN_LOADER(ll->data);
-			const gchar *ext_id = gplugin_loader_get_id(iter);
-
-			if(g_str_equal(id, ext_id)) {
-				existing = g_slist_remove(existing, iter);
-
-				g_object_unref(iter);
-
-				break;
-			}
-		}
-
-		existing = g_slist_prepend(existing, g_object_ref(loader));
-
-		/* Now insert the updated slist back into the hash table */
-		g_hash_table_insert(
-			manager->loaders_by_extension,
-			g_strdup(ext),
-			existing);
-	}
-	g_slist_free(exts);
-
 	/* make a note that we need to refresh */
 	manager->refresh_needed = TRUE;
 
@@ -884,7 +808,6 @@
 	GPluginLoader *loader,
 	GError **error)
 {
-	GSList *l = NULL, *exts = NULL;
 	const gchar *id = NULL;
 
 	g_return_val_if_fail(GPLUGIN_IS_MANAGER(manager), FALSE);
@@ -904,47 +827,6 @@
 		return FALSE;
 	}
 
-	exts = gplugin_loader_get_supported_extensions(loader);
-	for(l = exts; l; l = l->next) {
-		GSList *los = NULL;
-		GSList *ll = NULL;
-		const gchar *ext = NULL;
-
-		ext = (const gchar *)exts->data;
-		los = g_hash_table_lookup(manager->loaders_by_extension, ext);
-
-		for(ll = los; ll; ll = ll->next) {
-			GPluginLoader *lo = GPLUGIN_LOADER(ll->data);
-			const gchar *lo_id = gplugin_loader_get_id(lo);
-
-			/* check if this is not the loader we're looking for */
-			if(!g_str_equal(id, lo_id)) {
-				continue;
-			}
-
-			/* At this point, we're at the loader that we're removing. So we'll
-			 * remove it from the los SList. Then if the SList is empty, we
-			 * remove it from the hash table, otherwise we just update it.
-			 */
-			los = g_slist_remove(los, lo);
-			if(los) {
-				g_hash_table_insert(
-					manager->loaders_by_extension,
-					g_strdup(ext),
-					los);
-			} else {
-				g_hash_table_remove(manager->loaders_by_extension, ext);
-			}
-
-			/* kill our ref to the loader */
-			g_object_unref(lo);
-
-			/* now move to the next extension to check */
-			break;
-		}
-	}
-	g_slist_free(exts);
-
 	/* Temporarily add a reference to loader so we can emit the signal if it
 	 * was removed from our table correctly.
 	 */
@@ -985,231 +867,23 @@
 void
 gplugin_manager_refresh(GPluginManager *manager)
 {
-	GNode *root = NULL;
-	GList *error_messages = NULL, *l = NULL;
-	gchar *error_message = NULL;
-	guint errors = 0;
+	GPluginSource *file_source = NULL;
 
 	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));
 
-	/* build a tree of all possible plugins */
-	root = gplugin_file_tree_new(manager->paths->head);
+	file_source = gplugin_file_source_new(manager);
 
 	manager->refresh_needed = TRUE;
 
 	while(manager->refresh_needed) {
-		GNode *dir = NULL;
-
-		if(error_messages) {
-			for(l = error_messages; l; l = l->next)
-				g_free(l->data);
-			g_list_free(error_messages);
-			error_messages = NULL;
-		}
-
 		manager->refresh_needed = FALSE;
 
-		for(dir = root->children; dir; dir = dir->next) {
-			GPluginFileTreeEntry *e = dir->data;
-			GNode *file = NULL;
-			const gchar *path = e->filename;
-
-			for(file = dir->children; file; file = file->next) {
-				GPluginPlugin *plugin = NULL;
-				GPluginLoader *loader = NULL;
-				GError *error = NULL;
-				GSList *l = NULL;
-				gchar *filename = NULL;
-
-				e = (GPluginFileTreeEntry *)file->data;
-
-				/* Build the path and see if we need to probe it! */
-				filename = g_build_filename(path, e->filename, NULL);
-				plugin = g_hash_table_lookup(
-					manager->plugins_filename_view,
-					filename);
-
-				if(plugin && GPLUGIN_IS_PLUGIN(plugin)) {
-					GPluginPluginState state = gplugin_plugin_get_state(plugin);
-
-					/* The plugin is in our "view", check its state.  If it's
-					 * queried or loaded, move on to the next one.
-					 */
-					if(state == GPLUGIN_PLUGIN_STATE_QUERIED ||
-					   state == GPLUGIN_PLUGIN_STATE_LOADED) {
-						g_free(filename);
-						continue;
-					}
-				}
-
-				/* grab the list of loaders for this extension */
-				l = g_hash_table_lookup(
-					manager->loaders_by_extension,
-					e->extension);
-				for(; l; l = l->next) {
-					if(!GPLUGIN_IS_LOADER(l->data)) {
-						continue;
-					}
-
-					loader = GPLUGIN_LOADER(l->data);
-
-					/* Try to probe the plugin with the current loader */
-					plugin =
-						gplugin_loader_query_plugin(loader, filename, &error);
-
-					/* Check the GError, if it's set, output its message and
-					 * try the next loader.
-					 */
-					if(error) {
-						errors++;
-
-						error_message = g_strdup_printf(
-							_("failed to query '%s' with "
-							  "loader '%s': %s"),
-							filename,
-							G_OBJECT_TYPE_NAME(loader),
-							error->message);
-						error_messages =
-							g_list_prepend(error_messages, error_message);
-
-						g_error_free(error);
-						error = NULL;
-
-						loader = NULL;
-
-						continue;
-					}
-
-					/* if the plugin instance is good, then break out of this
-					 * loop.
-					 */
-					if(GPLUGIN_IS_PLUGIN(plugin)) {
-						break;
-					}
-
-					g_object_unref(G_OBJECT(plugin));
-
-					loader = NULL;
-				}
-
-				/* check if our plugin instance is good.  If it's not good we
-				 * don't need to do anything but free the filename which we'll
-				 * do later.
-				 */
-				if(GPLUGIN_IS_PLUGIN(plugin)) {
-					/* we have a good plugin, huzzah!  We need to add it to our
-					 * "view" as well as the main plugin hash table.
-					 */
-
-					/* we want the internal filename from the plugin to avoid
-					 * duplicate memory, so we need to grab it for the "view".
-					 */
-					gchar *real_filename = gplugin_plugin_get_filename(plugin);
-
-					/* we also need the GPluginPluginInfo to get the plugin's
-					 * ID for the key in our main hash table.
-					 */
-					GPluginPluginInfo *info = gplugin_plugin_get_info(plugin);
-
-					const gchar *id = gplugin_plugin_info_get_id(info);
-					GSList *l = NULL, *ll = NULL;
-					gboolean seen = FALSE;
-
-					/* throw a warning if the info->id is NULL */
-					if(id == NULL) {
-						error_message = g_strdup_printf(
-							_("Plugin %s has a NULL id."),
-							real_filename);
-						g_free(real_filename);
-						g_object_unref(G_OBJECT(info));
-
-						error_messages =
-							g_list_prepend(error_messages, error_message);
-
-						continue;
-					}
-
-					/* now insert into our view */
-					g_hash_table_replace(
-						manager->plugins_filename_view,
-						real_filename,
-						g_object_ref(G_OBJECT(plugin)));
-
-					/* Grab the list of plugins with our id and prepend the new
-					 * plugin to it before updating it.
-					 */
-					l = g_hash_table_lookup(manager->plugins, id);
-					for(ll = l; ll; ll = ll->next) {
-						GPluginPlugin *splugin = GPLUGIN_PLUGIN(ll->data);
-						gchar *sfilename = gplugin_plugin_get_filename(splugin);
-
-						if(!g_strcmp0(real_filename, sfilename))
-							seen = TRUE;
-
-						g_free(sfilename);
-					}
-					if(!seen) {
-						l = g_slist_prepend(l, g_object_ref(plugin));
-						g_hash_table_insert(manager->plugins, g_strdup(id), l);
-					}
-
-					/* check if the plugin is supposed to be loaded on query,
-					 * and if so, load it.
-					 */
-					if(gplugin_plugin_info_get_load_on_query(info)) {
-						GError *error = NULL;
-						gboolean loaded;
-
-						loaded =
-							gplugin_loader_load_plugin(loader, plugin, &error);
-
-						if(!loaded) {
-							error_message = g_strdup_printf(
-								_("failed to load %s during query: %s"),
-								filename,
-								(error) ? error->message : _("Unknown"));
-							error_messages =
-								g_list_prepend(error_messages, error_message);
-
-							errors++;
-
-							g_error_free(error);
-						}
-					} else {
-						/* if errors is greater than 0 set
-						 * manager->refresh_needed to TRUE.
-						 */
-						if(errors > 0) {
-							errors = 0;
-							manager->refresh_needed = TRUE;
-						}
-					}
-
-					g_object_unref(G_OBJECT(info));
-
-					/* since the plugin is now stored in our hash tables we
-					 * need to remove this function's reference to it.
-					 */
-					g_object_unref(G_OBJECT(plugin));
-				}
-
-				g_free(filename);
-			}
+		if(gplugin_source_scan(file_source)) {
+			manager->refresh_needed = TRUE;
 		}
 	}
 
-	if(error_messages) {
-		error_messages = g_list_reverse(error_messages);
-		for(l = error_messages; l; l = l->next) {
-			g_warning("%s", (gchar *)l->data);
-			g_free(l->data);
-		}
-
-		g_list_free(error_messages);
-	}
-
-	/* free the file tree */
-	gplugin_file_tree_free(root);
+	g_clear_object(&file_source);
 }
 
 /**
@@ -1813,6 +1487,34 @@
 }
 
 /**
+ * gplugin_manager_add_plugin:
+ * @manager: The instance.
+ * @id: The id of the plugin to add.
+ * @plugin: The plugin to add.
+ *
+ * Adds @plugin to @manager with @id. This should only be called by
+ * [iface@GPlugin.Source] implementations.
+ *
+ * Since: 0.39.0
+ */
+void
+gplugin_manager_add_plugin(
+	GPluginManager *manager,
+	const gchar *id,
+	GPluginPlugin *plugin)
+{
+	GSList *plugins = NULL;
+
+	g_return_if_fail(GPLUGIN_IS_MANAGER(manager));
+	g_return_if_fail(id != NULL);
+	g_return_if_fail(GPLUGIN_IS_PLUGIN(plugin));
+
+	plugins = g_hash_table_lookup(manager->plugins, id);
+	plugins = g_slist_prepend(plugins, g_object_ref(plugin));
+	g_hash_table_insert(manager->plugins, g_strdup(id), plugins);
+}
+
+/**
  * gplugin_manager_get_default:
  *
  * Gets the default plugin manager in GPlugin.
diff --git a/gplugin/gplugin-private.h b/gplugin/gplugin-private.h
--- a/gplugin/gplugin-private.h
+++ b/gplugin/gplugin-private.h
@@ -24,6 +24,7 @@
  * it.
  */
 #define GPLUGIN_GLOBAL_HEADER_INSIDE
+#include <gplugin/gplugin-manager.h>
 #include <gplugin/gplugin-plugin-info.h>
 #include <gplugin/gplugin-plugin.h>
 #undef GPLUGIN_GLOBAL_HEADER_INSIDE
@@ -39,6 +40,12 @@
 	const GValue *handler_return,
 	gpointer data);
 
+G_GNUC_INTERNAL
+void gplugin_manager_add_plugin(
+	GPluginManager *manager,
+	const gchar *id,
+	GPluginPlugin *plugin);
+
 G_END_DECLS
 
 #endif /* GPLUGIN_PRIVATE_H */
diff --git a/gplugin/meson.build b/gplugin/meson.build
--- a/gplugin/meson.build
+++ b/gplugin/meson.build
@@ -34,11 +34,13 @@
 ]
 
 GPLUGIN_PRIVATE_HEADERS = [
+	'gplugin-file-source.h',
 	'gplugin-file-tree.h',
 	'gplugin-source.h',
 ]
 
 GPLUGIN_PRIVATE_SOURCES = [
+	'gplugin-file-source.c',
 	'gplugin-file-tree.c',
 	'gplugin-source.c',
 ]
diff --git a/gplugin/tests/test-native-loader.c b/gplugin/tests/test-native-loader.c
--- a/gplugin/tests/test-native-loader.c
+++ b/gplugin/tests/test-native-loader.c
@@ -80,7 +80,7 @@
 {
 	g_test_trap_subprocess("/loaders/native/error/query/subprocess", 0, 0);
 
-	g_test_trap_assert_failed();
+	g_test_trap_assert_stderr("*expected error*");
 }
 
 /******************************************************************************
