[gnome-builder: 23/139] buffers: move IdeBuffer and accessories into libide-code



commit cbfd6cff042e7a5a3880f49c024b641d7b44ffce
Author: Christian Hergert <chergert redhat com>
Date:   Wed Jan 9 15:19:57 2019 -0800

    buffers: move IdeBuffer and accessories into libide-code
    
    This refactoring of the buffer layer includes a rewrite of the
    IdeBufferManager to vastly simplify its use case. Many of the specific
    features provided by the buffer manager will be split into self-contained
    plugins to avoid complicating the buffer layer directly.

 src/libide/buffers/OVERVIEW.md                   |   30 -
 src/libide/buffers/ide-buffer-addin.c            |  140 -
 src/libide/buffers/ide-buffer-addin.h            |   54 -
 src/libide/buffers/ide-buffer-change-monitor.c   |  136 -
 src/libide/buffers/ide-buffer-change-monitor.h   |   66 -
 src/libide/buffers/ide-buffer-manager.c          | 2394 --------------
 src/libide/buffers/ide-buffer-manager.h          |  117 -
 src/libide/buffers/ide-buffer-private.h          |   55 -
 src/libide/buffers/ide-buffer.c                  | 3600 ---------------------
 src/libide/buffers/ide-buffer.h                  |  178 --
 src/libide/buffers/ide-unsaved-files.h           |   81 -
 src/libide/buffers/meson.build                   |   32 -
 src/libide/code/ide-buffer-addin-private.h       |   82 +
 src/libide/code/ide-buffer-addin.c               |  411 +++
 src/libide/code/ide-buffer-addin.h               |   96 +
 src/libide/code/ide-buffer-change-monitor.c      |  233 ++
 src/libide/code/ide-buffer-change-monitor.h      |   86 +
 src/libide/code/ide-buffer-manager.c             | 1309 ++++++++
 src/libide/code/ide-buffer-manager.h             |  119 +
 src/libide/code/ide-buffer-private.h             |   64 +
 src/libide/code/ide-buffer.c                     | 3675 ++++++++++++++++++++++
 src/libide/code/ide-buffer.h                     |  178 ++
 src/libide/code/ide-unsaved-file-private.h       |   32 +
 src/libide/{buffers => code}/ide-unsaved-file.c  |   12 +-
 src/libide/{buffers => code}/ide-unsaved-file.h  |    8 +-
 src/libide/{buffers => code}/ide-unsaved-files.c |  139 +-
 src/libide/code/ide-unsaved-files.h              |   87 +
 27 files changed, 6491 insertions(+), 6923 deletions(-)
---
diff --git a/src/libide/code/ide-buffer-addin-private.h b/src/libide/code/ide-buffer-addin-private.h
new file mode 100644
index 000000000..ec6f3c6b4
--- /dev/null
+++ b/src/libide/code/ide-buffer-addin-private.h
@@ -0,0 +1,82 @@
+/* ide-buffer-addin-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-plugins.h>
+#include <libpeas/peas.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-addin.h"
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+  IdeBuffer   *buffer;
+  const gchar *language_id;
+} IdeBufferLanguageSet;
+
+typedef struct
+{
+  IdeBuffer *buffer;
+  GFile     *file;
+} IdeBufferFileSave;
+
+typedef struct
+{
+  IdeBuffer *buffer;
+  GFile     *file;
+} IdeBufferFileLoad;
+
+void _ide_buffer_addin_load_cb                 (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_unload_cb               (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_file_loaded_cb          (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_save_file_cb            (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_file_saved_cb           (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_language_set_cb         (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_change_settled_cb       (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_style_scheme_changed_cb (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer-addin.c b/src/libide/code/ide-buffer-addin.c
new file mode 100644
index 000000000..7eaf484b8
--- /dev/null
+++ b/src/libide/code/ide-buffer-addin.c
@@ -0,0 +1,411 @@
+/* ide-buffer-addin.c
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-buffer-addin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-addin.h"
+#include "ide-buffer-addin-private.h"
+#include "ide-buffer-private.h"
+
+/**
+ * SECTION:ide-buffer-addin
+ * @title: IdeBufferAddin
+ * @short_description: addins for #IdeBuffer
+ *
+ * The #IdeBufferAddin allows a plugin to register an object that will be
+ * created with every #IdeBuffer. It can register extra features with the
+ * buffer or extend it as necessary.
+ *
+ * Once use of #IdeBufferAddin is to add a spellchecker to the buffer that
+ * may be used by views to show the misspelled words. This is preferrable
+ * to adding a spellchecker in each view because it allows for multiple
+ * views to share one spellcheker on the underlying buffer.
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeBufferAddin, ide_buffer_addin, G_TYPE_OBJECT)
+
+static void
+ide_buffer_addin_default_init (IdeBufferAddinInterface *iface)
+{
+}
+
+/**
+ * ide_buffer_addin_load:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ *
+ * This calls the load virtual function of #IdeBufferAddin to request
+ * that the addin load itself.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_load (IdeBufferAddin *self,
+                       IdeBuffer      *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->load)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->load (self, buffer);
+}
+
+/**
+ * ide_buffer_addin_unload:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ *
+ * This calls the unload virtual function of #IdeBufferAddin to request
+ * that the addin unload itself.
+ *
+ * The addin should cancel any in-flight operations and attempt to drop
+ * references to the buffer or any other machinery as soon as possible.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_unload (IdeBufferAddin *self,
+                         IdeBuffer      *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->unload)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->unload (self, buffer);
+}
+
+/**
+ * ide_buffer_addin_file_loaded:
+ * @self: a #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ * @file: a #GFile
+ *
+ * This function is called for an addin after a file has been loaded from disk.
+ *
+ * It is not guaranteed that this function will be called for addins that were
+ * loaded after the buffer already loaded a file.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_file_loaded (IdeBufferAddin *self,
+                              IdeBuffer      *buffer,
+                              GFile          *file)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->file_loaded)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->file_loaded (self, buffer, file);
+}
+
+/**
+ * ide_buffer_addin_save_file:
+ * @self: a #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ * @file: a #GFile
+ *
+ * This function gives a chance for plugins to modify the buffer right before
+ * writing to disk.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_save_file (IdeBufferAddin *self,
+                            IdeBuffer      *buffer,
+                            GFile          *file)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->save_file)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->save_file (self, buffer, file);
+}
+
+/**
+ * ide_buffer_addin_file_saved:
+ * @self: a #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ * @file: a #GFile
+ *
+ * This function is called for an addin after a file has been saved to disk.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_file_saved (IdeBufferAddin *self,
+                             IdeBuffer      *buffer,
+                             GFile          *file)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->file_saved)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->file_saved (self, buffer, file);
+}
+
+/**
+ * ide_buffer_addin_language_set:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ * @language_id: the GtkSourceView language identifier
+ *
+ * This vfunc is called when the source language in the buffer changes. This
+ * will only be delivered to addins that support multiple languages.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_language_set (IdeBufferAddin *self,
+                               IdeBuffer      *buffer,
+                               const gchar    *language_id)
+{
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->language_set)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->language_set (self, buffer, language_id);
+}
+
+/**
+ * ide_buffer_addin_change_settled:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #ideBuffer
+ *
+ * This function is called when the buffer has settled after a number of
+ * changes provided by the user. It is a convenient way to know when you
+ * should perform more background work without having to coalesce work
+ * yourself.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_change_settled (IdeBufferAddin *self,
+                                 IdeBuffer      *buffer)
+{
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->change_settled)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->change_settled (self, buffer);
+}
+
+/**
+ * ide_buffer_addin_style_scheme_changed:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ *
+ * This function is called when the #GtkSourceStyleScheme of the #IdeBuffer
+ * has changed.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_style_scheme_changed (IdeBufferAddin *self,
+                                       IdeBuffer      *buffer)
+{
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->style_scheme_changed)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->style_scheme_changed (self, buffer);
+}
+
+/**
+ * ide_buffer_addin_find_by_module_name:
+ * @buffer: an #IdeBuffer
+ * @module_name: the module name of the addin
+ *
+ * Locates an addin attached to the #IdeBuffer by the name of the module
+ * that provides the addin.
+ *
+ * Returns: (transfer none) (nullable): An #IdeBufferAddin or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBufferAddin *
+ide_buffer_addin_find_by_module_name (IdeBuffer   *buffer,
+                                      const gchar *module_name)
+{
+  PeasPluginInfo *plugin_info;
+  IdeExtensionSetAdapter *set;
+  PeasExtension *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
+  g_return_val_if_fail (module_name != NULL, NULL);
+
+  set = _ide_buffer_get_addins (buffer);
+
+  /* Addins might not be loaded */
+  if (set == NULL)
+    return NULL;
+
+  plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name);
+
+  if (plugin_info != NULL)
+    ret = ide_extension_set_adapter_get_extension (set, plugin_info);
+  else
+    g_warning ("Failed to locate addin named %s", module_name);
+
+  return ret ? IDE_BUFFER_ADDIN (ret) : NULL;
+}
+
+void
+_ide_buffer_addin_load_cb (IdeExtensionSetAdapter *set,
+                           PeasPluginInfo         *plugin_info,
+                           PeasExtension          *exten,
+                           gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (IDE_IS_BUFFER (user_data));
+
+  ide_buffer_addin_load (IDE_BUFFER_ADDIN (exten), IDE_BUFFER (user_data));
+}
+
+void
+_ide_buffer_addin_unload_cb (IdeExtensionSetAdapter *set,
+                             PeasPluginInfo         *plugin_info,
+                             PeasExtension          *exten,
+                             gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (IDE_IS_BUFFER (user_data));
+
+  ide_buffer_addin_unload (IDE_BUFFER_ADDIN (exten), IDE_BUFFER (user_data));
+}
+
+void
+_ide_buffer_addin_file_loaded_cb (IdeExtensionSetAdapter *set,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *exten,
+                                  gpointer                user_data)
+{
+  IdeBufferFileLoad *load = user_data;
+
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (load != NULL);
+  g_return_if_fail (IDE_IS_BUFFER (load->buffer));
+  g_return_if_fail (G_IS_FILE (load->file));
+
+  ide_buffer_addin_file_loaded (IDE_BUFFER_ADDIN (exten), load->buffer, load->file);
+}
+
+void
+_ide_buffer_addin_save_file_cb (IdeExtensionSetAdapter *set,
+                                PeasPluginInfo         *plugin_info,
+                                PeasExtension          *exten,
+                                gpointer                user_data)
+{
+  IdeBufferFileSave *save = user_data;
+
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (save != NULL);
+  g_return_if_fail (IDE_IS_BUFFER (save->buffer));
+  g_return_if_fail (G_IS_FILE (save->file));
+
+  ide_buffer_addin_save_file (IDE_BUFFER_ADDIN (exten), save->buffer, save->file);
+}
+
+void
+_ide_buffer_addin_file_saved_cb (IdeExtensionSetAdapter *set,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *exten,
+                                 gpointer                user_data)
+{
+  IdeBufferFileSave *save = user_data;
+
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (save != NULL);
+  g_return_if_fail (IDE_IS_BUFFER (save->buffer));
+  g_return_if_fail (G_IS_FILE (save->file));
+
+  ide_buffer_addin_file_saved (IDE_BUFFER_ADDIN (exten), save->buffer, save->file);
+}
+
+void
+_ide_buffer_addin_language_set_cb (IdeExtensionSetAdapter *set,
+                                   PeasPluginInfo         *plugin_info,
+                                   PeasExtension          *exten,
+                                   gpointer                user_data)
+{
+  IdeBufferLanguageSet *lang = user_data;
+
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (lang != NULL);
+  g_return_if_fail (IDE_IS_BUFFER (lang->buffer));
+
+  ide_buffer_addin_language_set (IDE_BUFFER_ADDIN (exten), lang->buffer, lang->language_id);
+}
+
+void
+_ide_buffer_addin_change_settled_cb (IdeExtensionSetAdapter *set,
+                                     PeasPluginInfo         *plugin_info,
+                                     PeasExtension          *exten,
+                                     gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (IDE_IS_BUFFER (user_data));
+
+  ide_buffer_addin_change_settled (IDE_BUFFER_ADDIN (exten), IDE_BUFFER (user_data));
+}
+
+void
+_ide_buffer_addin_style_scheme_changed_cb (IdeExtensionSetAdapter *set,
+                                           PeasPluginInfo         *plugin_info,
+                                           PeasExtension          *exten,
+                                           gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (IDE_IS_BUFFER (user_data));
+
+  ide_buffer_addin_style_scheme_changed (IDE_BUFFER_ADDIN (exten), IDE_BUFFER (user_data));
+}
diff --git a/src/libide/code/ide-buffer-addin.h b/src/libide/code/ide-buffer-addin.h
new file mode 100644
index 000000000..78481188e
--- /dev/null
+++ b/src/libide/code/ide-buffer-addin.h
@@ -0,0 +1,96 @@
+/* ide-buffer-addin.h
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUFFER_ADDIN (ide_buffer_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeBufferAddin, ide_buffer_addin, IDE, BUFFER_ADDIN, GObject)
+
+struct _IdeBufferAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void (*load)                 (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer);
+  void (*unload)               (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer);
+  void (*file_loaded)          (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer,
+                                GFile             *file);
+  void (*save_file)            (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer,
+                                GFile             *file);
+  void (*file_saved)           (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer,
+                                GFile             *file);
+  void (*language_set)         (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer,
+                                const gchar       *language_id);
+  void (*change_settled)       (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer);
+  void (*style_scheme_changed) (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer);
+};
+
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_load                 (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_unload               (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_file_loaded          (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer,
+                                                       GFile          *file);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_save_file            (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer,
+                                                       GFile          *file);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_file_saved           (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer,
+                                                       GFile          *file);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_language_set         (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer,
+                                                       const gchar    *language_id);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_change_settled       (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_style_scheme_changed (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+IdeBufferAddin *ide_buffer_addin_find_by_module_name  (IdeBuffer      *buffer,
+                                                       const gchar    *module_name);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer-change-monitor.c b/src/libide/code/ide-buffer-change-monitor.c
new file mode 100644
index 000000000..b812bfb65
--- /dev/null
+++ b/src/libide/code/ide-buffer-change-monitor.c
@@ -0,0 +1,233 @@
+/* ide-buffer-change-monitor.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-buffer-change-monitor"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-change-monitor.h"
+#include "ide-buffer-private.h"
+
+typedef struct
+{
+  IdeBuffer *buffer;
+} IdeBufferChangeMonitorPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeBufferChangeMonitor, ide_buffer_change_monitor, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+IdeBufferLineChange
+ide_buffer_change_monitor_get_change (IdeBufferChangeMonitor *self,
+                                      guint                   line)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self), IDE_BUFFER_LINE_CHANGE_NONE);
+
+  if G_LIKELY (IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->get_change)
+    return IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->get_change (self, line);
+  else
+    return IDE_BUFFER_LINE_CHANGE_NONE;
+}
+
+static void
+ide_buffer_change_monitor_set_buffer (IdeBufferChangeMonitor *self,
+                                      IdeBuffer              *buffer)
+{
+  IdeBufferChangeMonitorPrivate *priv = ide_buffer_change_monitor_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  priv->buffer = g_object_ref (buffer);
+
+  if (IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->load)
+    IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->load (self, buffer);
+}
+
+void
+ide_buffer_change_monitor_emit_changed (IdeBufferChangeMonitor *self)
+{
+  IdeBufferChangeMonitorPrivate *priv = ide_buffer_change_monitor_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+
+  g_signal_emit (self, signals [CHANGED], 0);
+
+  if (priv->buffer)
+    _ide_buffer_line_flags_changed (priv->buffer);
+}
+
+static void
+ide_buffer_change_monitor_destroy (IdeObject *object)
+{
+  IdeBufferChangeMonitor *self = (IdeBufferChangeMonitor *)object;
+  IdeBufferChangeMonitorPrivate *priv = ide_buffer_change_monitor_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+
+  g_clear_object (&priv->buffer);
+
+  IDE_OBJECT_CLASS (ide_buffer_change_monitor_parent_class)->destroy (object);
+}
+
+static void
+ide_buffer_change_monitor_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  IdeBufferChangeMonitor *self = IDE_BUFFER_CHANGE_MONITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      g_value_set_object (value, ide_buffer_change_monitor_get_buffer (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_change_monitor_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  IdeBufferChangeMonitor *self = IDE_BUFFER_CHANGE_MONITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      ide_buffer_change_monitor_set_buffer (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_change_monitor_class_init (IdeBufferChangeMonitorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_buffer_change_monitor_get_property;
+  object_class->set_property = ide_buffer_change_monitor_set_property;
+
+  i_object_class->destroy = ide_buffer_change_monitor_destroy;
+
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer",
+                         "Buffer",
+                         "The IdeBuffer to be monitored.",
+                         IDE_TYPE_BUFFER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] = g_signal_new ("changed",
+                                    G_TYPE_FROM_CLASS (klass),
+                                    G_SIGNAL_RUN_LAST,
+                                    0,
+                                    NULL, NULL, NULL,
+                                    G_TYPE_NONE,
+                                    0);
+}
+
+static void
+ide_buffer_change_monitor_init (IdeBufferChangeMonitor *self)
+{
+}
+
+void
+ide_buffer_change_monitor_reload (IdeBufferChangeMonitor *self)
+{
+  g_return_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+
+  if (IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->reload)
+    IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->reload (self);
+}
+
+/**
+ * ide_buffer_change_monitor_foreach_change:
+ * @self: a #IdeBufferChangeMonitor
+ * @line_begin: the starting line
+ * @line_end: the end line
+ * @callback: (scope call): a callback
+ * @user_data: user data for @callback
+ *
+ * Calls @callback for every line between @line_begin and @line_end that have
+ * an addition, deletion, or change.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_change_monitor_foreach_change (IdeBufferChangeMonitor            *self,
+                                          guint                              line_begin,
+                                          guint                              line_end,
+                                          IdeBufferChangeMonitorForeachFunc  callback,
+                                          gpointer                           user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+  g_return_if_fail (callback != NULL);
+
+  if (IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->foreach_change)
+    IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->foreach_change (self, line_begin, line_end, callback, 
user_data);
+}
+
+/**
+ * ide_buffer_change_monitor_get_buffer:
+ * @self: a #IdeBufferChangeMonitor
+ *
+ * Gets the #IdeBufferChangeMonitor:buffer property.
+ *
+ * Returns: (transfer none): an #IdeBuffer
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_buffer_change_monitor_get_buffer (IdeBufferChangeMonitor *self)
+{
+  IdeBufferChangeMonitorPrivate *priv = ide_buffer_change_monitor_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self), NULL);
+
+  return priv->buffer;
+}
diff --git a/src/libide/code/ide-buffer-change-monitor.h b/src/libide/code/ide-buffer-change-monitor.h
new file mode 100644
index 000000000..3c48cf931
--- /dev/null
+++ b/src/libide/code/ide-buffer-change-monitor.h
@@ -0,0 +1,86 @@
+/* ide-buffer-change-monitor.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUFFER_CHANGE_MONITOR (ide_buffer_change_monitor_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeBufferChangeMonitor, ide_buffer_change_monitor, IDE, BUFFER_CHANGE_MONITOR, 
IdeObject)
+
+typedef enum
+{
+  IDE_BUFFER_LINE_CHANGE_NONE    = 0,
+  IDE_BUFFER_LINE_CHANGE_ADDED   = 1 << 0,
+  IDE_BUFFER_LINE_CHANGE_CHANGED = 1 << 1,
+  IDE_BUFFER_LINE_CHANGE_DELETED = 1 << 2,
+} IdeBufferLineChange;
+
+typedef void (*IdeBufferChangeMonitorForeachFunc) (guint               line,
+                                                   IdeBufferLineChange change,
+                                                   gpointer            user_data);
+
+struct _IdeBufferChangeMonitorClass
+{
+  IdeObjectClass parent_class;
+
+  void                (*load)           (IdeBufferChangeMonitor            *self,
+                                         IdeBuffer                         *buffer);
+  IdeBufferLineChange (*get_change)     (IdeBufferChangeMonitor            *self,
+                                         guint                              line);
+  void                (*reload)         (IdeBufferChangeMonitor            *self);
+  void                (*foreach_change) (IdeBufferChangeMonitor            *self,
+                                         guint                              line_begin,
+                                         guint                              line_end,
+                                         IdeBufferChangeMonitorForeachFunc  callback,
+                                         gpointer                           user_data);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeBuffer          *ide_buffer_change_monitor_get_buffer     (IdeBufferChangeMonitor            *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_buffer_change_monitor_emit_changed   (IdeBufferChangeMonitor            *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_buffer_change_monitor_foreach_change (IdeBufferChangeMonitor            *self,
+                                                              guint                              line_begin,
+                                                              guint                              line_end,
+                                                              IdeBufferChangeMonitorForeachFunc  callback,
+                                                              gpointer                           user_data);
+IDE_AVAILABLE_IN_3_32
+IdeBufferLineChange ide_buffer_change_monitor_get_change     (IdeBufferChangeMonitor            *self,
+                                                              guint                              line);
+IDE_AVAILABLE_IN_3_32
+void                ide_buffer_change_monitor_reload         (IdeBufferChangeMonitor            *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer-manager.c b/src/libide/code/ide-buffer-manager.c
new file mode 100644
index 000000000..76a206712
--- /dev/null
+++ b/src/libide/code/ide-buffer-manager.c
@@ -0,0 +1,1309 @@
+/* ide-buffer-manager.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-buffer-manager"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-private.h"
+#include "ide-buffer-manager.h"
+#include "ide-doc-seq-private.h"
+#include "ide-text-edit.h"
+#include "ide-text-edit-private.h"
+#include "ide-location.h"
+#include "ide-range.h"
+
+struct _IdeBufferManager
+{
+  IdeObject   parent_instance;
+  GHashTable *loading_tasks;
+  gssize      max_file_size;
+};
+
+typedef struct
+{
+  GPtrArray *buffers;
+  guint      n_active;
+  guint      had_failure : 1;
+} SaveAll;
+
+typedef struct
+{
+  GPtrArray  *edits;
+  GHashTable *buffers;
+  GHashTable *to_close;
+  guint       n_active;
+  guint       failed : 1;
+} EditState;
+
+typedef struct
+{
+  GFile *file;
+  IdeBuffer *buffer;
+} FindBuffer;
+
+typedef struct
+{
+  IdeBufferForeachFunc func;
+  gpointer user_data;
+} Foreach;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeBufferManager, ide_buffer_manager, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_MAX_FILE_SIZE,
+  N_PROPS
+};
+
+enum {
+  BUFFER_LOADED,
+  BUFFER_SAVED,
+  BUFFER_UNLOADED,
+  LOAD_BUFFER,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+edit_state_free (EditState *state)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  if (state != NULL)
+    {
+      g_clear_pointer (&state->edits, g_ptr_array_unref);
+      g_clear_pointer (&state->buffers, g_hash_table_unref);
+      g_clear_pointer (&state->to_close, g_hash_table_unref);
+      g_slice_free (EditState, state);
+    }
+}
+
+static void
+save_all_free (SaveAll *state)
+{
+  g_assert (state->n_active == 0);
+  g_clear_pointer (&state->buffers, g_ptr_array_unref);
+  g_slice_free (SaveAll, state);
+}
+
+static IdeBuffer *
+ide_buffer_manager_create_buffer (IdeBufferManager *self,
+                                  GFile            *file,
+                                  gboolean          is_temporary)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autoptr(IdeObjectBox) box = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+
+  buffer = _ide_buffer_new (self, file, is_temporary);
+  box = ide_object_box_new (G_OBJECT (buffer));
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (box));
+  _ide_buffer_attach (buffer, IDE_OBJECT (box));
+
+  IDE_RETURN (g_steal_pointer (&buffer));
+}
+
+static void
+ide_buffer_manager_add (IdeObject         *object,
+                        IdeObject         *sibling,
+                        IdeObject         *child,
+                        IdeObjectLocation  location)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+  g_autoptr(IdeBuffer) buffer = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (IDE_IS_OBJECT (child));
+
+  if (!IDE_IS_OBJECT_BOX (child) ||
+      !IDE_IS_BUFFER ((buffer = ide_object_box_ref_object (IDE_OBJECT_BOX (child)))))
+    {
+      g_critical ("You may only add an IdeObjectBox of IdeBuffer to an IdeBufferManager");
+      return;
+    }
+
+  IDE_OBJECT_CLASS (ide_buffer_manager_parent_class)->add (object, sibling, child, location);
+  g_list_model_items_changed (G_LIST_MODEL (self), ide_object_get_position (child), 0, 1);
+}
+
+static void
+ide_buffer_manager_remove (IdeObject *object,
+                           IdeObject *child)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  guint position;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (IDE_IS_OBJECT_BOX (child));
+
+  buffer = ide_object_box_ref_object (IDE_OBJECT_BOX (child));
+  g_signal_emit (self, signals [BUFFER_UNLOADED], 0, buffer);
+
+  position = ide_object_get_position (child);
+  IDE_OBJECT_CLASS (ide_buffer_manager_parent_class)->remove (object, child);
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0);
+}
+
+static void
+ide_buffer_manager_destroy (IdeObject *object)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+
+  IDE_ENTRY;
+
+  g_clear_pointer (&self->loading_tasks, g_hash_table_unref);
+
+  IDE_OBJECT_CLASS (ide_buffer_manager_parent_class)->destroy (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeBufferManager *self = IDE_BUFFER_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_FILE_SIZE:
+      g_value_set_int64 (value, ide_buffer_manager_get_max_file_size (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_manager_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeBufferManager *self = IDE_BUFFER_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_FILE_SIZE:
+      ide_buffer_manager_set_max_file_size (self, g_value_get_int64 (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_manager_class_init (IdeBufferManagerClass *klass)
+{
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_buffer_manager_get_property;
+  object_class->set_property = ide_buffer_manager_set_property;
+
+  i_object_class->add = ide_buffer_manager_add;
+  i_object_class->remove = ide_buffer_manager_remove;
+  i_object_class->destroy = ide_buffer_manager_destroy;
+
+  /**
+   * IdeBufferManager:max-file-size:
+   *
+   * The "max-file-size" property is the largest file size in bytes that
+   * Builder will attempt to load. Larger files will fail to load to help
+   * ensure that Builder's buffer manager does not attempt to load files that
+   * will slow the buffer management beyond usefulness.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_MAX_FILE_SIZE] =
+    g_param_spec_int64 ("max-file-size",
+                        "Max File Size",
+                        "The max file size to load",
+                        -1,
+                        G_MAXINT64,
+                        10L * 1024L * 1024L,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeBufferManager:load-buffer:
+   * @self: an #IdeBufferManager
+   * @buffer: an #IdeBuffer
+   * @create_new_view: if the buffer requires a view to be created
+   *
+   * The "load-buffer" signal is emitted before a buffer is (re)loaded.
+   *
+   * Since: 3.32
+   */
+  signals [LOAD_BUFFER] =
+    g_signal_new ("load-buffer",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE, 2, IDE_TYPE_BUFFER, G_TYPE_BOOLEAN);
+
+  /**
+   * IdeBufferManager::buffer-loaded:
+   * @self: an #IdeBufferManager
+   * @buffer: an #IdeBuffer
+   *
+   * The "buffer-loaded" signal is emitted when an #IdeBuffer has loaded
+   * a file from storage.
+   *
+   * Since: 3.32
+   */
+  signals [BUFFER_LOADED] =
+    g_signal_new ("buffer-loaded",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_BUFFER);
+  g_signal_set_va_marshaller (signals [BUFFER_LOADED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeBufferManager::buffer-saved:
+   * @self: an #IdeBufferManager
+   * @buffer: an #IdeBuffer
+   *
+   * The "buffer-saved" signal is emitted when an #IdeBuffer has been saved
+   * to storage.
+   *
+   * Since: 3.32
+   */
+  signals [BUFFER_SAVED] =
+    g_signal_new ("buffer-saved",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_BUFFER);
+  g_signal_set_va_marshaller (signals [BUFFER_SAVED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeBufferManager::buffer-unloaded:
+   * @self: an #IdeBufferManager
+   * @buffer: an #IdeBuffer
+   *
+   * The "buffer-unloaded" signal is emitted when an #IdeBuffer has been
+   * unloaded from the buffer manager.
+   *
+   * Since: 3.32
+   */
+  signals [BUFFER_UNLOADED] =
+    g_signal_new ("buffer-unloaded",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_BUFFER);
+  g_signal_set_va_marshaller (signals [BUFFER_UNLOADED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+}
+
+static void
+ide_buffer_manager_init (IdeBufferManager *self)
+{
+  IDE_ENTRY;
+
+  self->loading_tasks = g_hash_table_new_full (g_file_hash,
+                                               (GEqualFunc)g_file_equal,
+                                               g_object_unref,
+                                               g_object_unref);
+
+  IDE_EXIT;
+}
+
+static GFile *
+ide_buffer_manager_next_temp_file (IdeBufferManager *self)
+{
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) ret = NULL;
+  g_autofree gchar *name = NULL;
+  guint doc_id;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), NULL);
+
+  context = IDE_CONTEXT (ide_object_ref_root (IDE_OBJECT (self)));
+  workdir = ide_context_ref_workdir (context);
+  doc_id = ide_doc_seq_acquire ();
+
+  /* translators: %u is replaced with an incrementing number */
+  name = g_strdup_printf (_("unsaved file %u"), doc_id);
+
+  ret = g_file_get_child (workdir, name);
+
+  IDE_RETURN (g_steal_pointer (&ret));
+}
+
+/**
+ * ide_buffer_manager_from_context:
+ *
+ * Gets the #IdeBufferManager for the #IdeContext.
+ *
+ * Returns: (transfer none): an #IdeBufferManager
+ *
+ * Since: 3.32
+ */
+IdeBufferManager *
+ide_buffer_manager_from_context (IdeContext *context)
+{
+  IdeBufferManager *self;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  self = ide_context_peek_child_typed (context, IDE_TYPE_BUFFER_MANAGER);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), NULL);
+  return self;
+}
+
+/**
+ * ide_buffer_manager_has_file:
+ * @self: an #IdeBufferManager
+ * @file: a #GFile
+ *
+ * Checks to see if a buffer has been loaded which contains the contents
+ * of @file.
+ *
+ * Returns: %TRUE if a buffer exists for @file
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_manager_has_file (IdeBufferManager *self,
+                             GFile            *file)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+
+  return ide_buffer_manager_find_buffer (self, file) != NULL;
+}
+
+static void
+ide_buffer_manager_find_buffer_cb (IdeObject  *object,
+                                   FindBuffer *find)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+
+  g_assert (IDE_IS_OBJECT_BOX (object));
+  g_assert (find != NULL);
+  g_assert (G_IS_FILE (find->file));
+
+  if (find->buffer != NULL)
+    return;
+
+  buffer = ide_object_box_ref_object (IDE_OBJECT_BOX (object));
+
+  /* We pass back a borrowed reference */
+  if (g_file_equal (find->file, ide_buffer_get_file (buffer)))
+    find->buffer = buffer;
+}
+
+/**
+ * ide_buffer_manager_find_buffer:
+ * @self: an #IdeBufferManager
+ * @file: a #GFile
+ *
+ * Locates the #IdeBuffer that matches #GFile, if any.
+ *
+ * Returns: (transfer full): an #IdeBuffer or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_buffer_manager_find_buffer (IdeBufferManager *self,
+                                GFile            *file)
+{
+  FindBuffer find = { file, NULL };
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  ide_object_foreach (IDE_OBJECT (self),
+                      (GFunc)ide_buffer_manager_find_buffer_cb,
+                      &find);
+
+  IDE_RETURN (find.buffer);
+}
+
+/**
+ * ide_buffer_manager_get_max_file_size:
+ * @self: an #IdeBufferManager
+ *
+ * Gets the max file size that will be allowed to be loaded from disk.
+ * This is useful to protect Builder from files that would overload the
+ * various subsystems.
+ *
+ * Returns: the max file size in bytes or -1 for unlimited
+ *
+ * Since: 3.32
+ */
+gssize
+ide_buffer_manager_get_max_file_size (IdeBufferManager *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), 0);
+
+  return self->max_file_size;
+}
+
+/**
+ * ide_buffer_manager_set_max_size:
+ * @self: an #IdeBufferManager
+ * @max_file_size: the max file size in bytes or -1 for unlimited
+ *
+ * Sets the max file size that will be allowed to be loaded from disk.
+ * This is useful to protect Builder from files that would overload the
+ * various subsystems.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_set_max_file_size (IdeBufferManager *self,
+                                      gssize            max_file_size)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (max_file_size >= -1);
+
+  if (self->max_file_size != max_file_size)
+    {
+      self->max_file_size = max_file_size;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MAX_FILE_SIZE]);
+    }
+}
+
+static void
+ide_buffer_manager_load_file_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeBuffer *buffer = (IdeBuffer *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBufferManager *self;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  file = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (G_IS_FILE (file));
+
+  g_hash_table_remove (self->loading_tasks, file);
+
+  if (!_ide_buffer_load_file_finish (buffer, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_object (task, g_object_ref (buffer));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_manager_load_file_async:
+ * @self: an #IdeBufferManager
+ * @file: (nullable): a #GFile
+ * @flags: optional flags for loading the buffer
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @notif: (out) (optional): a location for an #IdeNotification, or %NULL
+ * @callback: a callback to execute upon completion of the operation
+ * @user_data: closure data for @callback
+ *
+ * Requests that @file be loaded by the buffer manager. Depending on @flags,
+ * this may result in a new view being displayed in a Builder workspace.
+ *
+ * If @file is %NULL, then a new temporary file is created with an
+ * incrementing number to denote the document, such as "unsaved file 1".
+ *
+ * After completion, @callback will be executed and you can receive the buffer
+ * that was loaded with ide_buffer_manager_load_file_finish().
+ *
+ * If a buffer has already been loaded from @file, the operation will complete
+ * using that existing buffer.
+ *
+ * If a buffer is currently loading for @file, the operation will complete
+ * using that existing buffer after it has completed loading.
+ *
+ * If @notif is non-NULL, it will be set to a new #IdeNotification which should
+ * be freed with g_object_unref() when no longer in use. It will be kept up to
+ * date with loading progress as the file is loaded.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_load_file_async (IdeBufferManager     *self,
+                                    GFile                *file,
+                                    IdeBufferOpenFlags    flags,
+                                    GCancellable         *cancellable,
+                                    IdeNotification     **notif,
+                                    GAsyncReadyCallback   callback,
+                                    gpointer              user_data)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) temp_file = NULL;
+  IdeBuffer *existing;
+  gboolean create_new_view = FALSE;
+  gboolean is_new = FALSE;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (!file || G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (notif != NULL)
+    *notif = NULL;
+
+  if (file == NULL)
+    file = temp_file = ide_buffer_manager_next_temp_file (self);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_manager_load_file_async);
+  ide_task_set_task_data (task, g_file_dup (file), g_object_unref);
+
+  /* If the file requested has already been opened, then we will return
+   * that (unless a forced reload was requested).
+   */
+  if ((existing = ide_buffer_manager_find_buffer (self, file)))
+    {
+      IdeTask *existing_task;
+
+      /* If the buffer does not need to be reloaded, just return the
+       * buffer to the user now.
+       */
+      if (!(flags & IDE_BUFFER_OPEN_FLAGS_FORCE_RELOAD))
+        {
+          ide_task_return_object (task, g_object_ref (existing));
+          IDE_EXIT;
+        }
+
+      /* If the buffer is still loading, we can just chain onto that
+       * loading operation and complete this task when that task finishes.
+       */
+      if ((existing_task = g_hash_table_lookup (self->loading_tasks, file)))
+        {
+          ide_task_chain (existing_task, task);
+          IDE_EXIT;
+        }
+    }
+  else
+    {
+      /* Create the buffer and track it so we can find it later */
+      buffer = ide_buffer_manager_create_buffer (self, file, temp_file != NULL);
+      is_new = TRUE;
+    }
+
+  /* Save this task for later in case we get in a second request to open
+   * the file while we are already opening it.
+   */
+  g_hash_table_insert (self->loading_tasks, g_file_dup (file), g_object_ref (task));
+
+  /* We might have listeners tracking new buffers. Apply some rules to
+   * determine if we need the UI to create a new view for the buffer.
+   */
+  create_new_view = !(flags & IDE_BUFFER_OPEN_FLAGS_NO_VIEW) &&
+                     (is_new || (flags & IDE_BUFFER_OPEN_FLAGS_BACKGROUND) == 0);
+  g_signal_emit (self, signals [LOAD_BUFFER], 0, buffer, create_new_view);
+
+  /* Now we can load the buffer asynchronously */
+  _ide_buffer_load_file_async (buffer,
+                               cancellable,
+                               notif,
+                               ide_buffer_manager_load_file_cb,
+                               g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_manager_load_file_finish:
+ * @self: an #IdeBufferManager
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_buffer_manager_laod_file_async().
+ *
+ * Returns: (transfer full): an #IdeBuffer
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_buffer_manager_load_file_finish (IdeBufferManager  *self,
+                                     GAsyncResult      *result,
+                                     GError           **error)
+{
+  IdeBuffer *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_object (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_buffer_manager_save_all_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeBuffer *buffer = (IdeBuffer *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  SaveAll *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+
+  g_assert (state != NULL);
+  g_assert (state->buffers != NULL);
+  g_assert (state->n_active > 0);
+
+  if (!ide_buffer_save_file_finish (buffer, result, &error))
+    {
+      g_warning ("Failed to save buffer ā€œ%sā€: %s",
+                 ide_buffer_dup_title (buffer),
+                 error->message);
+      state->had_failure = TRUE;
+    }
+
+  state->n_active--;
+
+  if (state->n_active == 0)
+    {
+      if (state->had_failure)
+        ide_task_return_new_error (task,
+                                   G_IO_ERROR,
+                                   G_IO_ERROR_FAILED,
+                                   "One or more buffers failed to save");
+      else
+        ide_task_return_boolean (task, TRUE);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_save_all_foreach_cb (IdeObject *object,
+                                        IdeTask   *task)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+  SaveAll *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT_BOX (object));
+  g_assert (IDE_IS_TASK (task));
+
+  buffer = ide_object_box_ref_object (IDE_OBJECT_BOX (object));
+  state = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (state != NULL);
+  g_assert (state->buffers != NULL);
+
+  /* Skip buffers that are loading or saving, as they are already in
+   * the correct form on disk (or will be soon). We somewhat risk beating
+   * an existing save, but that is probably okay to the user since they've
+   * already submitted the save request.
+   */
+  if (ide_buffer_get_state (buffer) != IDE_BUFFER_STATE_READY)
+    return;
+
+  g_ptr_array_add (state->buffers, g_object_ref (buffer));
+
+  state->n_active++;
+
+  ide_buffer_save_file_async (buffer,
+                              NULL,
+                              ide_task_get_cancellable (task),
+                              NULL,
+                              ide_buffer_manager_save_all_cb,
+                              g_object_ref (task));
+}
+
+/**
+ * ide_buffer_manager_save_all_async:
+ * @self: an #IdeBufferManager
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests that the #IdeBufferManager save all of the loaded
+ * buffers to disk.
+ *
+ * @callback will be executed after all the buffers have been saved.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_save_all_async (IdeBufferManager    *self,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  SaveAll *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_manager_save_all_async);
+
+  state = g_slice_new0 (SaveAll);
+  state->buffers = g_ptr_array_new_full (0, g_object_unref);
+  ide_task_set_task_data (task, state, save_all_free);
+
+  ide_object_foreach (IDE_OBJECT (self),
+                      (GFunc)ide_buffer_manager_save_all_foreach_cb,
+                      task);
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_manager_save_all_finish:
+ * @self: an #IdeBufferManager
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NUL
+ *
+ * Completes an asynchronous request to save all buffers.
+ *
+ * Returns: %TRUE if all the buffers were saved successfully
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_manager_save_all_finish (IdeBufferManager  *self,
+                                    GAsyncResult      *result,
+                                    GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_buffer_manager_apply_edits_save_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_buffer_manager_save_all_finish (self, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_do_apply_edits (IdeBufferManager *self,
+                                   GHashTable       *buffers,
+                                   GPtrArray        *edits)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (buffers != NULL);
+  g_assert (edits != NULL);
+
+  /* Allow each project edit to stage its GtkTextMarks */
+  for (guint i = 0; i < edits->len; i++)
+    {
+      IdeTextEdit *edit = g_ptr_array_index (edits, i);
+      IdeLocation *location;
+      IdeRange *range;
+      IdeBuffer *buffer;
+      GFile *file;
+
+      if (NULL == (range = ide_text_edit_get_range (edit)) ||
+          NULL == (location = ide_range_get_begin (range)) ||
+          NULL == (file = ide_location_get_file (location)) ||
+          NULL == (buffer = g_hash_table_lookup (buffers, file)))
+        {
+          g_warning ("Implausible failure to access buffer");
+          continue;
+        }
+
+      gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+
+      _ide_text_edit_prepare (edit, buffer);
+    }
+
+  /* Now actually perform the replacement between the text marks */
+  for (guint i = 0; i < edits->len; i++)
+    {
+      IdeTextEdit *edit = g_ptr_array_index (edits, i);
+      IdeLocation *location;
+      IdeRange *range;
+      IdeBuffer *buffer;
+      GFile *file;
+
+      if (NULL == (range = ide_text_edit_get_range (edit)) ||
+          NULL == (location = ide_range_get_begin (range)) ||
+          NULL == (file = ide_location_get_file (location)) ||
+          NULL == (buffer = g_hash_table_lookup (buffers, file)))
+        {
+          g_warning ("Implausible failure to access buffer");
+          continue;
+        }
+
+      _ide_text_edit_apply (edit, buffer);
+    }
+
+  /* Complete all of our undo groups */
+  for (guint i = 0; i < edits->len; i++)
+    {
+      IdeTextEdit *edit = g_ptr_array_index (edits, i);
+      IdeLocation *location;
+      IdeRange *range;
+      IdeBuffer *buffer;
+      GFile *file;
+
+      if (NULL == (range = ide_text_edit_get_range (edit)) ||
+          NULL == (location = ide_range_get_begin (range)) ||
+          NULL == (file = ide_location_get_file (location)) ||
+          NULL == (buffer = g_hash_table_lookup (buffers, file)))
+        {
+          g_warning ("Implausible failure to access buffer");
+          continue;
+        }
+
+      gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_apply_edits_buffer_loaded_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  GCancellable *cancellable;
+  EditState *state;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  cancellable = ide_task_get_cancellable (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (state != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  state->n_active--;
+
+  /* Get our buffer, if we failed, we won't proceed with edits */
+  if (!(buffer = ide_buffer_manager_load_file_finish (self, result, &error)))
+    {
+      if (state->failed == FALSE)
+        {
+          state->failed = TRUE;
+          ide_task_return_error (task, g_steal_pointer (&error));
+        }
+    }
+
+  /* Nothing to do if we already failed */
+  if (state->failed)
+    IDE_EXIT;
+
+  /* Save the buffer for future use when applying edits */
+  file = ide_buffer_get_file (buffer);
+  g_hash_table_insert (state->buffers, g_object_ref (file), g_object_ref (buffer));
+  g_hash_table_insert (state->to_close, g_object_ref (file), g_object_ref (buffer));
+
+  /* If this is the last buffer to load, then we can go apply the edits. */
+  if (state->n_active == 0)
+    {
+      ide_buffer_manager_do_apply_edits (self,
+                                         state->buffers,
+                                         state->edits);
+      ide_buffer_manager_save_all_async (self,
+                                         cancellable,
+                                         ide_buffer_manager_apply_edits_save_cb,
+                                         g_steal_pointer (&task));
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_apply_edits_completed_cb (IdeBufferManager *self,
+                                             GParamSpec       *pspec,
+                                             IdeTask          *task)
+{
+  GHashTableIter iter;
+  IdeBuffer *buffer;
+  EditState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+  g_assert (state != NULL);
+  g_assert (state->to_close != NULL);
+  g_assert (state->buffers != NULL);
+
+  g_hash_table_iter_init (&iter, state->to_close);
+
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&buffer))
+    {
+      IdeObjectBox *box = ide_object_box_from_object (G_OBJECT (buffer));
+
+      g_assert (IDE_IS_OBJECT_BOX (box));
+
+      ide_object_destroy (IDE_OBJECT (box));
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_manager_apply_edits_async:
+ * @self: An #IdeBufferManager
+ * @edits: (transfer full) (element-type IdeTextEdit):
+ *   An #GPtrArray of #IdeTextEdit.
+ * @cancellable: (allow-none): a #GCancellable or %NULL
+ * @callback: the callback to complete the request
+ * @user_data: user data for @callback
+ *
+ * Asynchronously requests that all of @edits are applied to the buffers
+ * in the project. If the buffer has not been loaded for a particular edit,
+ * it will be loaded.
+ *
+ * @callback should call ide_buffer_manager_apply_edits_finish() to get the
+ * result of this operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_apply_edits_async (IdeBufferManager    *self,
+                                      GPtrArray           *edits,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  EditState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (edits != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (edits, g_object_unref);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_manager_apply_edits_async);
+
+  state = g_slice_new0 (EditState);
+  state->buffers = g_hash_table_new_full (g_file_hash,
+                                          (GEqualFunc)g_file_equal,
+                                          g_object_unref,
+                                          _g_object_unref0);
+  state->to_close = g_hash_table_new_full (g_file_hash,
+                                           (GEqualFunc)g_file_equal,
+                                           g_object_unref,
+                                           _g_object_unref0);
+  state->edits = g_steal_pointer (&edits);
+  ide_task_set_task_data (task, state, edit_state_free);
+
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_buffer_manager_apply_edits_completed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  for (guint i = 0; i < state->edits->len; i++)
+    {
+      IdeTextEdit *edit = g_ptr_array_index (state->edits, i);
+      IdeLocation *location;
+      IdeBuffer *buffer;
+      IdeRange *range;
+      GFile *file;
+
+      if (NULL == (range = ide_text_edit_get_range (edit)) ||
+          NULL == (location = ide_range_get_begin (range)) ||
+          NULL == (file = ide_location_get_file (location)))
+        continue;
+
+      if (g_hash_table_contains (state->buffers, file))
+        continue;
+
+      if ((buffer = ide_buffer_manager_find_buffer (self, file)))
+        {
+          g_hash_table_insert (state->buffers, g_object_ref (file), g_object_ref (buffer));
+          continue;
+        }
+
+      g_hash_table_insert (state->buffers, g_object_ref (file), NULL);
+
+      state->n_active++;
+
+      /* Load buffers, but don't create views for them since we don't want to
+       * create lots of views if there are lots of files to edit.
+       */
+      ide_buffer_manager_load_file_async (self,
+                                          file,
+                                          IDE_BUFFER_OPEN_FLAGS_NO_VIEW,
+                                          cancellable,
+                                          NULL,
+                                          ide_buffer_manager_apply_edits_buffer_loaded_cb,
+                                          g_object_ref (task));
+    }
+
+  IDE_TRACE_MSG ("Waiting for %d buffers to load", state->n_active);
+
+  if (state->n_active == 0)
+    {
+      ide_buffer_manager_do_apply_edits (self, state->buffers, state->edits);
+      ide_buffer_manager_save_all_async (self,
+                                         cancellable,
+                                         ide_buffer_manager_apply_edits_save_cb,
+                                         g_steal_pointer (&task));
+    }
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_buffer_manager_apply_edits_finish (IdeBufferManager  *self,
+                                       GAsyncResult      *result,
+                                       GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+void
+_ide_buffer_manager_buffer_loaded (IdeBufferManager *self,
+                                   IdeBuffer        *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  g_signal_emit (self, signals [BUFFER_LOADED], 0, buffer);
+}
+
+void
+_ide_buffer_manager_buffer_saved (IdeBufferManager *self,
+                                  IdeBuffer        *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  g_signal_emit (self, signals [BUFFER_SAVED], 0, buffer);
+}
+
+static GType
+ide_buffer_manager_get_item_type (GListModel *model)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (model));
+
+  return IDE_TYPE_BUFFER;
+}
+
+static gpointer
+ide_buffer_manager_get_item (GListModel *model,
+                             guint       position)
+{
+  IdeBufferManager *self = (IdeBufferManager *)model;
+  g_autoptr(IdeObject) box = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+
+  if ((box = ide_object_get_nth_child (IDE_OBJECT (self), position)))
+    return ide_object_box_ref_object (IDE_OBJECT_BOX (box));
+
+  return NULL;
+}
+
+static guint
+ide_buffer_manager_get_n_items (GListModel *model)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (model));
+
+  return ide_object_get_n_children (IDE_OBJECT (model));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_buffer_manager_get_item_type;
+  iface->get_item = ide_buffer_manager_get_item;
+  iface->get_n_items = ide_buffer_manager_get_n_items;
+}
+
+static void
+ide_buffer_manager_foreach_cb (IdeObject *object,
+                               gpointer   user_data)
+{
+  const Foreach *state = user_data;
+
+  g_assert (IDE_IS_OBJECT (object));
+
+  if (IDE_IS_BUFFER (object))
+    state->func (IDE_BUFFER (object), state->user_data);
+}
+
+/**
+ * ide_buffer_manager_foreach:
+ * @self: a #IdeBufferManager
+ * @foreach_func: (scope call): an #IdeBufferForeachFunc
+ * @user_data: closure data for @foreach_func
+ *
+ * Calls @foreach_func for every buffer registered.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_foreach (IdeBufferManager     *self,
+                            IdeBufferForeachFunc  foreach_func,
+                            gpointer              user_data)
+{
+  Foreach state = { foreach_func, user_data };
+
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (foreach_func != NULL);
+
+  ide_object_foreach (IDE_OBJECT (self),
+                      (GFunc)ide_buffer_manager_foreach_cb,
+                      &state);
+}
diff --git a/src/libide/code/ide-buffer-manager.h b/src/libide/code/ide-buffer-manager.h
new file mode 100644
index 000000000..3bbc43342
--- /dev/null
+++ b/src/libide/code/ide-buffer-manager.h
@@ -0,0 +1,119 @@
+/* ide-buffer-manager.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-buffer.h"
+
+G_BEGIN_DECLS
+
+/**
+ * IdeBufferOpenFlags:
+ * @IDE_BUFFER_OPEN_FLAGS_NONE: No special processing will be performed
+ * @IDE_BUFFER_OPEN_FLAGS_BACKGROUND: Open the document in the background (behind current view)
+ * @IDE_BUFFER_OPEN_FLAGS_NO_VIEW: Open the document but do not create a new view for it
+ *
+ * The #IdeBufferOpenFlags enumeration is used to specify how a document should
+ * be opened by the workbench. Plugins may want to have a bit of control over
+ * where the document is opened, and this provides a some control over that.
+ *
+ * Since: 3.32
+ */
+typedef enum
+{
+  IDE_BUFFER_OPEN_FLAGS_NONE         = 0,
+  IDE_BUFFER_OPEN_FLAGS_BACKGROUND   = 1 << 0,
+  IDE_BUFFER_OPEN_FLAGS_NO_VIEW      = 1 << 1,
+  IDE_BUFFER_OPEN_FLAGS_FORCE_RELOAD = 1 << 2,
+} IdeBufferOpenFlags;
+
+/**
+ * IdeBufferForeachFunc:
+ * @buffer: an #IdeBuffer
+ * @user_data: closure data
+ *
+ * Callback prototype for ide_buffer_manager_foreach().
+ *
+ * Since: 3.32
+ */
+typedef void (*IdeBufferForeachFunc) (IdeBuffer *buffer,
+                                      gpointer   user_data);
+
+#define IDE_TYPE_BUFFER_MANAGER (ide_buffer_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeBufferManager, ide_buffer_manager, IDE, BUFFER_MANAGER, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeBufferManager *ide_buffer_manager_from_context       (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_foreach            (IdeBufferManager     *self,
+                                                         IdeBufferForeachFunc  foreach_func,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_load_file_async    (IdeBufferManager     *self,
+                                                         GFile                *file,
+                                                         IdeBufferOpenFlags    flags,
+                                                         GCancellable         *cancellable,
+                                                         IdeNotification     **notif,
+                                                         GAsyncReadyCallback   callback,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeBuffer        *ide_buffer_manager_load_file_finish   (IdeBufferManager     *self,
+                                                         GAsyncResult         *result,
+                                                         GError              **error);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_save_all_async     (IdeBufferManager     *self,
+                                                         GCancellable         *cancellable,
+                                                         GAsyncReadyCallback   callback,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_buffer_manager_save_all_finish    (IdeBufferManager     *self,
+                                                         GAsyncResult         *result,
+                                                         GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_buffer_manager_has_file           (IdeBufferManager     *self,
+                                                         GFile                *file);
+IDE_AVAILABLE_IN_3_32
+IdeBuffer        *ide_buffer_manager_find_buffer        (IdeBufferManager     *self,
+                                                         GFile                *file);
+IDE_AVAILABLE_IN_3_32
+gssize            ide_buffer_manager_get_max_file_size  (IdeBufferManager     *self);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_set_max_file_size  (IdeBufferManager     *self,
+                                                         gssize                max_file_size);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_apply_edits_async  (IdeBufferManager     *self,
+                                                         GPtrArray            *edits,
+                                                         GCancellable         *cancellable,
+                                                         GAsyncReadyCallback   callback,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_buffer_manager_apply_edits_finish (IdeBufferManager     *self,
+                                                         GAsyncResult         *result,
+                                                         GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer-private.h b/src/libide/code/ide-buffer-private.h
new file mode 100644
index 000000000..836fda7d7
--- /dev/null
+++ b/src/libide/code/ide-buffer-private.h
@@ -0,0 +1,64 @@
+/* ide-buffer-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+#include <libide-plugins.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-manager.h"
+#include "ide-highlight-engine.h"
+
+G_BEGIN_DECLS
+
+void                    _ide_buffer_manager_buffer_loaded (IdeBufferManager     *self,
+                                                           IdeBuffer            *buffer);
+void                    _ide_buffer_manager_buffer_saved  (IdeBufferManager     *self,
+                                                           IdeBuffer            *buffer);
+void                    _ide_buffer_cancel_cursor_restore (IdeBuffer            *self);
+gboolean                _ide_buffer_can_restore_cursor    (IdeBuffer            *self);
+IdeExtensionSetAdapter *_ide_buffer_get_addins            (IdeBuffer            *self);
+IdeBuffer              *_ide_buffer_new                   (IdeBufferManager     *self,
+                                                           GFile                *file,
+                                                           gboolean              is_temporary);
+void                    _ide_buffer_attach                (IdeBuffer            *self,
+                                                           IdeObject            *parent);
+void                    _ide_buffer_load_file_async       (IdeBuffer            *self,
+                                                           GCancellable         *cancellable,
+                                                           IdeNotification     **notif,
+                                                           GAsyncReadyCallback   callback,
+                                                           gpointer              user_data);
+gboolean                _ide_buffer_load_file_finish      (IdeBuffer            *self,
+                                                           GAsyncResult         *result,
+                                                           GError              **error);
+void                    _ide_buffer_line_flags_changed    (IdeBuffer            *self);
+void                    _ide_buffer_set_changed_on_volume (IdeBuffer            *self,
+                                                           gboolean              changed_on_volume);
+void                    _ide_buffer_set_read_only         (IdeBuffer            *self,
+                                                           gboolean              read_only);
+IdeHighlightEngine     *_ide_buffer_get_highlight_engine  (IdeBuffer            *self);
+void                    _ide_buffer_set_failure           (IdeBuffer            *self,
+                                                           const GError         *error);
+void                    _ide_buffer_sync_to_unsaved_files (IdeBuffer            *self);
+void                    _ide_buffer_set_file              (IdeBuffer            *self,
+                                                           GFile                *file);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer.c b/src/libide/code/ide-buffer.c
new file mode 100644
index 000000000..6ba46952c
--- /dev/null
+++ b/src/libide/code/ide-buffer.c
@@ -0,0 +1,3675 @@
+/* ide-buffer.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-buffer"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-addin.h"
+#include "ide-buffer-addin-private.h"
+#include "ide-buffer-manager.h"
+#include "ide-buffer-private.h"
+#include "ide-code-enums.h"
+#include "ide-diagnostic.h"
+#include "ide-diagnostics.h"
+#include "ide-file-settings.h"
+#include "ide-formatter.h"
+#include "ide-formatter-options.h"
+#include "ide-location.h"
+#include "ide-highlight-engine.h"
+#include "ide-range.h"
+#include "ide-source-iter.h"
+#include "ide-source-style-scheme.h"
+#include "ide-symbol-resolver.h"
+#include "ide-unsaved-files.h"
+
+#define SETTLING_DELAY_MSEC  333
+
+#define TAG_ERROR            "diagnostician::error"
+#define TAG_WARNING          "diagnostician::warning"
+#define TAG_DEPRECATED       "diagnostician::deprecated"
+#define TAG_NOTE             "diagnostician::note"
+#define TAG_SNIPPET_TAB_STOP "snippet::tab-stop"
+#define TAG_DEFINITION       "action::hover-definition"
+#define TAG_CURRENT_BKPT     "debugger::current-breakpoint"
+
+#define DEPRECATED_COLOR     "#babdb6"
+#define ERROR_COLOR          "#ff0000"
+#define NOTE_COLOR           "#708090"
+#define WARNING_COLOR        "#fcaf3e"
+#define CURRENT_BKPT_FG      "#fffffe"
+#define CURRENT_BKPT_BG      "#fcaf3e"
+
+struct _IdeBuffer
+{
+  GtkSourceBuffer         parent_instance;
+
+  /* Owned references */
+  IdeExtensionSetAdapter *addins;
+  IdeExtensionSetAdapter *symbol_resolvers;
+  IdeExtensionAdapter    *rename_provider;
+  IdeExtensionAdapter    *formatter;
+  IdeBufferManager       *buffer_manager;
+  IdeBufferChangeMonitor *change_monitor;
+  GBytes                 *content;
+  IdeDiagnostics         *diagnostics;
+  GError                 *failure;
+  IdeFileSettings        *file_settings;
+  IdeHighlightEngine     *highlight_engine;
+  GtkSourceFile          *source_file;
+
+  /* Scalars */
+  guint                   change_count;
+  guint                   settling_source;
+  gint                    hold;
+
+  /* Bit-fields */
+  IdeBufferState          state : 3;
+  guint                   can_restore_cursor : 1;
+  guint                   is_temporary : 1;
+  guint                   changed_on_volume : 1;
+  guint                   read_only : 1;
+  guint                   highlight_diagnostics : 1;
+};
+
+typedef struct
+{
+  IdeNotification *notif;
+  GFile           *file;
+  guint            highlight_syntax : 1;
+} LoadState;
+
+typedef struct
+{
+  GFile           *file;
+  IdeNotification *notif;
+} SaveState;
+
+typedef struct
+{
+  GPtrArray   *resolvers;
+  IdeLocation *location;
+  IdeSymbol   *symbol;
+} LookUpSymbolData;
+
+G_DEFINE_TYPE (IdeBuffer, ide_buffer, GTK_SOURCE_TYPE_BUFFER)
+
+enum {
+  PROP_0,
+  PROP_BUFFER_MANAGER,
+  PROP_CHANGE_MONITOR,
+  PROP_CHANGED_ON_VOLUME,
+  PROP_DIAGNOSTICS,
+  PROP_FAILED,
+  PROP_FILE,
+  PROP_FILE_SETTINGS,
+  PROP_HAS_DIAGNOSTICS,
+  PROP_HAS_SYMBOL_RESOLVERS,
+  PROP_HIGHLIGHT_DIAGNOSTICS,
+  PROP_IS_TEMPORARY,
+  PROP_LANGUAGE_ID,
+  PROP_READ_ONLY,
+  PROP_STATE,
+  PROP_STYLE_SCHEME_NAME,
+  PROP_TITLE,
+  N_PROPS
+};
+
+enum {
+  CHANGE_SETTLED,
+  CURSOR_MOVED,
+  LINE_FLAGS_CHANGED,
+  LOADED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void     lookup_symbol_data_free            (LookUpSymbolData       *data);
+static void     apply_style                        (GtkTextTag             *tag,
+                                                    const gchar            *first_property,
+                                                    ...);
+static void     load_state_free                    (LoadState              *state);
+static void     save_state_free                    (SaveState              *state);
+static void     ide_buffer_save_file_cb            (GObject                *object,
+                                                    GAsyncResult           *result,
+                                                    gpointer                user_data);
+static void     ide_buffer_load_file_cb            (GObject                *object,
+                                                    GAsyncResult           *result,
+                                                    gpointer                user_data);
+static void     ide_buffer_progress_cb             (goffset                 current_num_bytes,
+                                                    goffset                 total_num_bytes,
+                                                    gpointer                user_data);
+static void     ide_buffer_get_property            (GObject                *object,
+                                                    guint                   prop_id,
+                                                    GValue                 *value,
+                                                    GParamSpec             *pspec);
+static void     ide_buffer_set_property            (GObject                *object,
+                                                    guint                   prop_id,
+                                                    const GValue           *value,
+                                                    GParamSpec             *pspec);
+static void     ide_buffer_constructed             (GObject                *object);
+static void     ide_buffer_dispose                 (GObject                *object);
+static void     ide_buffer_notify_language         (IdeBuffer              *self,
+                                                    GParamSpec             *pspec,
+                                                    gpointer                user_data);
+static void     ide_buffer_notify_style_scheme     (IdeBuffer              *self,
+                                                    GParamSpec             *pspec,
+                                                    gpointer                unused);
+static void     ide_buffer_reload_file_settings    (IdeBuffer              *self);
+static void     ide_buffer_set_file_settings       (IdeBuffer              *self,
+                                                    IdeFileSettings        *file_settings);
+static void     ide_buffer_emit_cursor_moved       (IdeBuffer              *self);
+static void     ide_buffer_changed                 (GtkTextBuffer          *buffer);
+static void     ide_buffer_delete_range            (GtkTextBuffer          *buffer,
+                                                    GtkTextIter            *start,
+                                                    GtkTextIter            *end);
+static void     ide_buffer_insert_text             (GtkTextBuffer          *buffer,
+                                                    GtkTextIter            *location,
+                                                    const gchar            *text,
+                                                    gint                    len);
+static void     ide_buffer_mark_set                (GtkTextBuffer          *buffer,
+                                                    const GtkTextIter      *iter,
+                                                    GtkTextMark            *mark);
+static void     ide_buffer_delay_settling          (IdeBuffer              *self);
+static gboolean ide_buffer_settled_cb              (gpointer                user_data);
+static void     ide_buffer_apply_diagnostics       (IdeBuffer              *self);
+static void     ide_buffer_clear_diagnostics       (IdeBuffer              *self);
+static void     ide_buffer_apply_diagnostic        (IdeBuffer              *self,
+                                                    IdeDiagnostic          *diagnostics);
+static void     ide_buffer_init_tags               (IdeBuffer              *self);
+static void     ide_buffer_on_tag_added            (IdeBuffer              *self,
+                                                    GtkTextTag             *tag,
+                                                    GtkTextTagTable        *table);
+static void     ide_buffer_get_symbol_resolvers_cb (IdeExtensionSetAdapter *set,
+                                                    PeasPluginInfo         *plugin_info,
+                                                    PeasExtension          *exten,
+                                                    gpointer                user_data);
+static void     ide_buffer_symbol_resolver_removed (IdeExtensionSetAdapter *adapter,
+                                                    PeasPluginInfo         *plugin_info,
+                                                    PeasExtension          *extension,
+                                                    gpointer                user_data);
+static void     ide_buffer_symbol_resolver_added   (IdeExtensionSetAdapter *adapter,
+                                                    PeasPluginInfo         *plugin_info,
+                                                    PeasExtension          *extension,
+                                                    gpointer                user_data);
+static gboolean ide_buffer_can_do_newline_hack     (IdeBuffer              *self,
+                                                    guint                   len);
+static void     ide_buffer_guess_language          (IdeBuffer              *self);
+static void     ide_buffer_real_loaded             (IdeBuffer              *self);
+
+static void
+load_state_free (LoadState *state)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (state != NULL);
+
+  g_clear_object (&state->notif);
+  g_clear_object (&state->file);
+  g_slice_free (LoadState, state);
+}
+
+static void
+save_state_free (SaveState *state)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (state != NULL);
+
+  g_clear_object (&state->notif);
+  g_clear_object (&state->file);
+  g_slice_free (SaveState, state);
+}
+
+static void
+lookup_symbol_data_free (LookUpSymbolData *data)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_pointer (&data->resolvers, g_ptr_array_unref);
+  g_clear_object (&data->location);
+  g_clear_object (&data->symbol);
+  g_slice_free (LookUpSymbolData, data);
+}
+
+IdeBuffer *
+_ide_buffer_new (IdeBufferManager *buffer_manager,
+                 GFile            *file,
+                 gboolean          is_temporary)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (buffer_manager), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  return g_object_new (IDE_TYPE_BUFFER,
+                       "buffer-manager", buffer_manager,
+                       "file", file,
+                       "is-temporary", is_temporary,
+                       NULL);
+}
+
+void
+_ide_buffer_set_file (IdeBuffer *self,
+                      GFile     *file)
+{
+  GFile *location;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  location = gtk_source_file_get_location (self->source_file);
+
+  if (location == NULL || !g_file_equal (file, location))
+    {
+      gtk_source_file_set_location (self->source_file, file);
+      ide_buffer_reload_file_settings (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE]);
+    }
+}
+
+static void
+ide_buffer_set_state (IdeBuffer      *self,
+                      IdeBufferState  state)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (state == IDE_BUFFER_STATE_READY ||
+                    state == IDE_BUFFER_STATE_LOADING ||
+                    state == IDE_BUFFER_STATE_SAVING ||
+                    state == IDE_BUFFER_STATE_FAILED);
+
+  if (self->state != state)
+    {
+      self->state = state;
+      if (self->state != IDE_BUFFER_STATE_FAILED)
+        g_clear_pointer (&self->failure, g_error_free);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATE]);
+    }
+}
+
+static void
+ide_buffer_real_loaded (IdeBuffer *self)
+{
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (self->buffer_manager != NULL)
+    _ide_buffer_manager_buffer_loaded (self->buffer_manager, self);
+}
+
+static void
+ide_buffer_notify_language (IdeBuffer  *self,
+                            GParamSpec *pspec,
+                            gpointer    user_data)
+{
+  const gchar *lang_id;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  ide_buffer_reload_file_settings (self);
+
+  lang_id = ide_buffer_get_language_id (self);
+
+  if (self->addins != NULL)
+    {
+      IdeBufferLanguageSet state = { self, lang_id };
+
+      ide_extension_set_adapter_set_value (self->addins, state.language_id);
+      ide_extension_set_adapter_foreach (self->addins,
+                                         _ide_buffer_addin_language_set_cb,
+                                         &state);
+    }
+
+  if (self->symbol_resolvers)
+    ide_extension_set_adapter_set_value (self->symbol_resolvers, lang_id);
+
+  if (self->rename_provider)
+    ide_extension_adapter_set_value (self->rename_provider, lang_id);
+
+  if (self->formatter)
+    ide_extension_adapter_set_value (self->formatter, lang_id);
+}
+
+static void
+ide_buffer_constructed (GObject *object)
+{
+  IdeBuffer *self = (IdeBuffer *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  G_OBJECT_CLASS (ide_buffer_parent_class)->constructed (object);
+
+  ide_buffer_init_tags (self);
+}
+
+static void
+ide_buffer_dispose (GObject *object)
+{
+  IdeBuffer *self = (IdeBuffer *)object;
+  IdeObjectBox *box;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_handle_id (&self->settling_source, g_source_remove);
+
+  /* Remove ourselves from the object-tree if necessary */
+  if ((box = ide_object_box_from_object (object)) &&
+      !ide_object_in_destruction (IDE_OBJECT (box)))
+    ide_object_destroy (IDE_OBJECT (box));
+
+  ide_clear_and_destroy_object (&self->addins);
+  ide_clear_and_destroy_object (&self->rename_provider);
+  ide_clear_and_destroy_object (&self->symbol_resolvers);
+  ide_clear_and_destroy_object (&self->formatter);
+  ide_clear_and_destroy_object (&self->highlight_engine);
+  g_clear_object (&self->buffer_manager);
+  ide_clear_and_destroy_object (&self->change_monitor);
+  g_clear_pointer (&self->content, g_bytes_unref);
+  g_clear_object (&self->diagnostics);
+  ide_clear_and_destroy_object (&self->file_settings);
+
+  G_OBJECT_CLASS (ide_buffer_parent_class)->dispose (object);
+}
+
+static void
+ide_buffer_finalize (GObject *object)
+{
+  IdeBuffer *self = (IdeBuffer *)object;
+
+  g_clear_object (&self->source_file);
+  g_clear_pointer (&self->failure, g_error_free);
+
+  G_OBJECT_CLASS (ide_buffer_parent_class)->finalize (object);
+}
+
+static void
+ide_buffer_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  IdeBuffer *self = IDE_BUFFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CHANGE_MONITOR:
+      g_value_set_object (value, ide_buffer_get_change_monitor (self));
+      break;
+
+    case PROP_CHANGED_ON_VOLUME:
+      g_value_set_boolean (value, ide_buffer_get_changed_on_volume (self));
+      break;
+
+    case PROP_DIAGNOSTICS:
+      g_value_set_object (value, ide_buffer_get_diagnostics (self));
+      break;
+
+    case PROP_FAILED:
+      g_value_set_boolean (value, ide_buffer_get_failed (self));
+      break;
+
+    case PROP_FILE:
+      g_value_set_object (value, ide_buffer_get_file (self));
+      break;
+
+    case PROP_FILE_SETTINGS:
+      g_value_set_object (value, ide_buffer_get_file_settings (self));
+      break;
+
+    case PROP_HAS_DIAGNOSTICS:
+      g_value_set_boolean (value, ide_buffer_has_diagnostics (self));
+      break;
+
+    case PROP_HAS_SYMBOL_RESOLVERS:
+      g_value_set_boolean (value, ide_buffer_has_symbol_resolvers (self));
+      break;
+
+    case PROP_HIGHLIGHT_DIAGNOSTICS:
+      g_value_set_boolean (value, ide_buffer_get_highlight_diagnostics (self));
+      break;
+
+    case PROP_LANGUAGE_ID:
+      g_value_set_string (value, ide_buffer_get_language_id (self));
+      break;
+
+    case PROP_IS_TEMPORARY:
+      g_value_set_boolean (value, ide_buffer_get_is_temporary (self));
+      break;
+
+    case PROP_READ_ONLY:
+      g_value_set_boolean (value, ide_buffer_get_read_only (self));
+      break;
+
+    case PROP_STATE:
+      g_value_set_enum (value, ide_buffer_get_state (self));
+      break;
+
+    case PROP_STYLE_SCHEME_NAME:
+      g_value_set_string (value, ide_buffer_get_style_scheme_name (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_take_string (value, ide_buffer_dup_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  IdeBuffer *self = IDE_BUFFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER_MANAGER:
+      self->buffer_manager = g_value_dup_object (value);
+      break;
+
+    case PROP_CHANGE_MONITOR:
+      ide_buffer_set_change_monitor (self, g_value_get_object (value));
+      break;
+
+    case PROP_DIAGNOSTICS:
+      ide_buffer_set_diagnostics (self, g_value_get_object (value));
+      break;
+
+    case PROP_FILE:
+      _ide_buffer_set_file (self, g_value_get_object (value));
+      break;
+
+    case PROP_HIGHLIGHT_DIAGNOSTICS:
+      ide_buffer_set_highlight_diagnostics (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_LANGUAGE_ID:
+      ide_buffer_set_language_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_IS_TEMPORARY:
+      self->is_temporary = g_value_get_boolean (value);
+      break;
+
+    case PROP_STYLE_SCHEME_NAME:
+      ide_buffer_set_style_scheme_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_class_init (IdeBufferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkTextBufferClass *buffer_class = GTK_TEXT_BUFFER_CLASS (klass);
+
+  object_class->constructed = ide_buffer_constructed;
+  object_class->dispose = ide_buffer_dispose;
+  object_class->finalize = ide_buffer_finalize;
+  object_class->get_property = ide_buffer_get_property;
+  object_class->set_property = ide_buffer_set_property;
+
+  buffer_class->changed = ide_buffer_changed;
+  buffer_class->delete_range = ide_buffer_delete_range;
+  buffer_class->insert_text = ide_buffer_insert_text;
+  buffer_class->mark_set = ide_buffer_mark_set;
+
+  /**
+   * IdeBuffer:buffer-manager:
+   *
+   * Sets the "buffer-manager" property, which is used by the buffer to
+   * clean-up state when the buffer is no longer in use.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BUFFER_MANAGER] =
+    g_param_spec_object ("buffer-manager",
+                         "Buffer Manager",
+                         "The buffer manager for the context.",
+                         IDE_TYPE_BUFFER_MANAGER,
+                         (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:change-monitor:
+   *
+   * The "change-monitor" property is an #IdeBufferChangeMonitor that will be
+   * used to track changes in the #IdeBuffer. This can be used to show line
+   * changes in the editor gutter.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CHANGE_MONITOR] =
+    g_param_spec_object ("change-monitor",
+                         "Change Monitor",
+                         "Change Monitor",
+                         IDE_TYPE_BUFFER_CHANGE_MONITOR,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:changed-on-volume:
+   *
+   * The "changed-on-volume" property is set to %TRUE when it has been
+   * discovered that the file represented by the #IdeBuffer has changed
+   * externally to Builder.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CHANGED_ON_VOLUME] =
+    g_param_spec_boolean ("changed-on-volume",
+                          "Changed On Volume",
+                          "If the buffer has been modified externally",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:diagnostics:
+   *
+   * The "diagnostics" property contains an #IdeDiagnostics that represent
+   * the diagnostics found in the buffer.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DIAGNOSTICS] =
+    g_param_spec_object ("diagnostics",
+                         "Diagnostics",
+                         "The diagnostics for the buffer",
+                         IDE_TYPE_DIAGNOSTICS,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:failed:
+   *
+   * The "failed" property is %TRUE when the buffer has entered a failed
+   * state such as when loading or saving the buffer to disk.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_FAILED] =
+    g_param_spec_boolean ("failed",
+                          "Failed",
+                          "If the buffer has entered a failed state",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:file:
+   *
+   * The "file" property is the underlying file represented by the buffer.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "The file the buffer represents",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:file-settings:
+   *
+   * The "file-settings" property are the settings to be used by the buffer
+   * and source-view for the underlying file.
+   *
+   * These are automatically discovered and kept up to date based on the
+   * #IdeFileSettings extension points.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_FILE_SETTINGS] =
+    g_param_spec_object ("file-settings",
+                         "File Settings",
+                         "The file settings for the buffer",
+                         IDE_TYPE_FILE_SETTINGS,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:has-diagnostics:
+   *
+   * The "has-diagnostics" property denotes that there are a non-zero number
+   * of diangostics registered for the buffer.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_DIAGNOSTICS] =
+    g_param_spec_boolean ("has-diagnostics",
+                          "Has Diagnostics",
+                          "The diagnostics for the buffer",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:has-symbol-resolvers:
+   *
+   * The "has-symbol-resolvers" property is %TRUE if there are any symbol
+   * resolvers loaded.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_SYMBOL_RESOLVERS] =
+    g_param_spec_boolean ("has-symbol-resolvers",
+                          "Has symbol resolvers",
+                          "If there is at least one symbol resolver available",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:highlight-diagnostics:
+   *
+   * The "highlight-diagnostics" property indicates that diagnostics which
+   * are discovered should be styled.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HIGHLIGHT_DIAGNOSTICS] =
+    g_param_spec_boolean ("highlight-diagnostics",
+                          "Highlight Diagnostics",
+                          "If diagnostics should be highlighted",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:is-temporary:
+   *
+   * The "is-temporary" property denotes the #IdeBuffer:file property points
+   * to a temporary file. When saving the the buffer, various UI components
+   * know to check this property and provide a file chooser to allow the user
+   * to select the destination file.
+   *
+   * Upon saving the file, the property will change to %FALSE.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_IS_TEMPORARY] =
+    g_param_spec_boolean ("is-temporary",
+                          "Is Temporary",
+                          "If the file property is a temporary file",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:language-id:
+   *
+   * The "language-id" property is a convenience property to set the
+   * #GtkSourceBuffer:langauge property using a string name.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_LANGUAGE_ID] =
+    g_param_spec_string ("language-id",
+                         "Language Id",
+                         "The language identifier as a string",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:read-only:
+   *
+   * The "read-only" property is set to %TRUE when it has been
+   * discovered that the file represented by the #IdeBuffer is read-only
+   * on the underlying storage.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_READ_ONLY] =
+    g_param_spec_boolean ("read-only",
+                          "Read Only",
+                          "If the buffer's file is read-only",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:state:
+   *
+   * The "state" property can be used to determine if the buffer is
+   * currently performing any specific background work, such as loading
+   * from or saving a buffer to storage.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_STATE] =
+    g_param_spec_enum ("state",
+                       "State",
+                       "The state for the buffer",
+                       IDE_TYPE_BUFFER_STATE,
+                       IDE_BUFFER_STATE_READY,
+                       (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:style-scheme-name:
+   *
+   * The "style-scheme-name" is the name of the style scheme that is used.
+   * It is a convenience property so that you do not need to use the
+   * #GtkSourceStyleSchemeManager to lookup style schemes.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_STYLE_SCHEME_NAME] =
+    g_param_spec_string ("style-scheme-name",
+                         "Style Scheme Name",
+                         "The name of the GtkSourceStyleScheme to use",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:title:
+   *
+   * The "title" for the buffer which includes some variant of the path
+   * to the underlying file.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title for the buffer",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeBuffer::change-settled:
+   * @self: an #IdeBuffer
+   *
+   * The "change-settled" signal is emitted when the buffer has stopped
+   * being edited for a short period of time. This is useful to connect
+   * to when you want to perform work as the user is editing, but you
+   * don't want to get in the way of their editing.
+   *
+   * Since: 3.32
+   */
+  signals [CHANGE_SETTLED] =
+    g_signal_new ("change-settled",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [CHANGE_SETTLED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  /**
+   * IdeBuffer::cursor-moved:
+   * @self: an #IdeBuffer
+   * @location: a #GtkTextIter
+   *
+   * This signal is emitted when the insertion location has moved. You might
+   * want to attach to this signal to update the location of the insert mark in
+   * the display.
+   *
+   * Since: 3.32
+   */
+  signals [CURSOR_MOVED] =
+    g_signal_new ("cursor-moved",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__BOXED,
+                  G_TYPE_NONE,
+                  1,
+                  GTK_TYPE_TEXT_ITER | G_SIGNAL_TYPE_STATIC_SCOPE);
+  g_signal_set_va_marshaller (signals [CURSOR_MOVED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__BOXEDv);
+
+  /**
+   * IdeBuffer::line-flags-changed:
+   * @self: an #IdeBuffer
+   *
+   * The "line-flags-changed" signal is emitted when the buffer has detected
+   * ancillary information has changed for lines in the buffer. Such information
+   * might include diagnostics or version control information.
+   *
+   * Since: 3.32
+   */
+  signals [LINE_FLAGS_CHANGED] =
+    g_signal_new_class_handler ("line-flags-changed",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL,
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [LINE_FLAGS_CHANGED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  /**
+   * IdeBuffer::loaded:
+   * @self: an #IdeBuffer
+   *
+   * The "loaded" signal is emitted after the buffer is loaded.
+   *
+   * This is useful to watch if you want to perform a given action but do
+   * not want to interfere with buffer loading.
+   *
+   * Since: 3.32
+   */
+  signals [LOADED] =
+    g_signal_new_class_handler ("loaded",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_buffer_real_loaded),
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [LOADED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+}
+
+static void
+ide_buffer_init (IdeBuffer *self)
+{
+  self->source_file = gtk_source_file_new ();
+  self->can_restore_cursor = TRUE;
+  self->highlight_diagnostics = TRUE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_signal_connect (self,
+                    "notify::language",
+                    G_CALLBACK (ide_buffer_notify_language),
+                    NULL);
+
+  g_signal_connect (self,
+                    "notify::style-scheme",
+                    G_CALLBACK (ide_buffer_notify_style_scheme),
+                    NULL);
+}
+
+static void
+ide_buffer_rename_provider_notify_extension (IdeBuffer           *self,
+                                             GParamSpec          *pspec,
+                                             IdeExtensionAdapter *adapter)
+{
+  IdeRenameProvider *provider;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (IDE_IS_EXTENSION_ADAPTER (adapter));
+
+  if ((provider = ide_extension_adapter_get_extension (adapter)))
+    {
+      g_object_set (provider, "buffer", self, NULL);
+      ide_rename_provider_load (provider);
+    }
+}
+
+static void
+ide_buffer_formatter_notify_extension (IdeBuffer           *self,
+                                       GParamSpec          *pspec,
+                                       IdeExtensionAdapter *adapter)
+{
+  IdeFormatter *formatter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (IDE_IS_EXTENSION_ADAPTER (adapter));
+
+  if ((formatter = ide_extension_adapter_get_extension (adapter)))
+    ide_formatter_load (formatter);
+}
+
+static void
+ide_buffer_symbol_resolver_added (IdeExtensionSetAdapter *adapter,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *extension,
+                                  gpointer                user_data)
+{
+  IdeSymbolResolver *resolver = (IdeSymbolResolver *)extension;
+  IdeBuffer *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
+  g_assert (IDE_IS_BUFFER (self));
+
+  IDE_TRACE_MSG ("Loading symbol resolver %s", G_OBJECT_TYPE_NAME (resolver));
+
+  ide_symbol_resolver_load (resolver);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_SYMBOL_RESOLVERS]);
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_symbol_resolver_removed (IdeExtensionSetAdapter *adapter,
+                                    PeasPluginInfo         *plugin_info,
+                                    PeasExtension          *extension,
+                                    gpointer                user_data)
+{
+  IdeSymbolResolver *resolver = (IdeSymbolResolver *)extension;
+  IdeBuffer *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
+  g_assert (IDE_IS_BUFFER (self));
+
+  IDE_TRACE_MSG ("Unloading symbol resolver %s", G_OBJECT_TYPE_NAME (resolver));
+
+  ide_symbol_resolver_unload (resolver);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_SYMBOL_RESOLVERS]);
+
+  IDE_EXIT;
+}
+
+void
+_ide_buffer_attach (IdeBuffer *self,
+                    IdeObject *parent)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_OBJECT_BOX (parent));
+  g_return_if_fail (ide_object_box_contains (IDE_OBJECT_BOX (parent), self));
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (self->addins == NULL);
+  g_return_if_fail (self->highlight_engine == NULL);
+  g_return_if_fail (self->formatter == NULL);
+  g_return_if_fail (self->rename_provider == NULL);
+
+  /* Setup the semantic highlight engine */
+  self->highlight_engine = ide_highlight_engine_new (self);
+
+  /* Load buffer addins */
+  self->addins = ide_extension_set_adapter_new (parent,
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_BUFFER_ADDIN,
+                                                "Buffer-Addin-Languages",
+                                                ide_buffer_get_language_id (self));
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (_ide_buffer_addin_load_cb),
+                    self);
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (_ide_buffer_addin_unload_cb),
+                    self);
+  ide_extension_set_adapter_foreach (self->addins,
+                                     _ide_buffer_addin_load_cb,
+                                     self);
+
+  /* Setup our rename provider, if any */
+  self->rename_provider = ide_extension_adapter_new (parent,
+                                                     peas_engine_get_default (),
+                                                     IDE_TYPE_RENAME_PROVIDER,
+                                                     "Rename-Provider-Languages",
+                                                     ide_buffer_get_language_id (self));
+  g_signal_connect_object (self->rename_provider,
+                           "notify::extension",
+                           G_CALLBACK (ide_buffer_rename_provider_notify_extension),
+                           self,
+                           G_CONNECT_SWAPPED);
+  ide_buffer_rename_provider_notify_extension (self, NULL, self->rename_provider);
+
+  /* Setup our formatter, if any */
+  self->formatter = ide_extension_adapter_new (parent,
+                                               peas_engine_get_default (),
+                                               IDE_TYPE_FORMATTER,
+                                               "Formatter-Languages",
+                                               ide_buffer_get_language_id (self));
+  g_signal_connect_object (self->formatter,
+                           "notify::extension",
+                           G_CALLBACK (ide_buffer_formatter_notify_extension),
+                           self,
+                           G_CONNECT_SWAPPED);
+  ide_buffer_formatter_notify_extension (self, NULL, self->formatter);
+
+  /* Setup symbol resolvers */
+  self->symbol_resolvers = ide_extension_set_adapter_new (parent,
+                                                          peas_engine_get_default (),
+                                                          IDE_TYPE_SYMBOL_RESOLVER,
+                                                          "Symbol-Resolver-Languages",
+                                                          ide_buffer_get_language_id (self));
+  g_signal_connect_object (self->symbol_resolvers,
+                           "extension-added",
+                           G_CALLBACK (ide_buffer_symbol_resolver_added),
+                           self,
+                           0);
+  g_signal_connect_object (self->symbol_resolvers,
+                           "extension-removed",
+                           G_CALLBACK (ide_buffer_symbol_resolver_removed),
+                           self,
+                           0);
+  ide_extension_set_adapter_foreach (self->symbol_resolvers,
+                                     ide_buffer_symbol_resolver_added,
+                                     self);
+}
+
+/**
+ * ide_buffer_get_file:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:file property.
+ *
+ * Returns: (transfer none): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_buffer_get_file (IdeBuffer *self)
+{
+  GFile *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  ret = gtk_source_file_get_location (self->source_file);
+
+  g_return_val_if_fail (G_IS_FILE (ret), NULL);
+
+  return ret;
+}
+
+/**
+ * ide_buffer_dup_uri:
+ * @self: a #IdeBuffer
+ *
+ * Gets the URI for the underlying file and returns a copy of it.
+ *
+ * Returns: (transfer full): a new string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_buffer_dup_uri (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return g_file_get_uri (ide_buffer_get_file (self));
+}
+
+/**
+ * ide_buffer_get_is_temporary:
+ *
+ * Checks if the buffer represents a temporary file.
+ *
+ * This is useful to check by views that want to provide a save-as dialog
+ * when the user requests to save the buffer.
+ *
+ * Returns: %TRUE if the buffer is for a temporary file
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_is_temporary (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->is_temporary;
+}
+
+/**
+ * ide_buffer_get_state:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:state property.
+ *
+ * This will changed while files are loaded or saved to disk.
+ *
+ * Returns: an #IdeBufferState
+ *
+ * Since: 3.32
+ */
+IdeBufferState
+ide_buffer_get_state (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), 0);
+
+  return self->state;
+}
+
+static void
+ide_buffer_progress_cb (goffset  current_num_bytes,
+                        goffset  total_num_bytes,
+                        gpointer user_data)
+{
+  IdeNotification *notif = user_data;
+  gdouble progress = 0.0;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  if (total_num_bytes)
+    progress = (gdouble)current_num_bytes / (gdouble)total_num_bytes;
+
+  ide_notification_set_progress (notif, progress);
+}
+
+static void
+ide_buffer_load_file_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  GtkSourceFileLoader *loader = (GtkSourceFileLoader *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GtkTextIter iter;
+  LoadState *state;
+  IdeBuffer *self;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_SOURCE_IS_FILE_LOADER (loader));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (state != NULL);
+  g_assert (G_IS_FILE (state->file));
+  g_assert (IDE_IS_NOTIFICATION (state->notif));
+
+  if (!gtk_source_file_loader_load_finish (loader, result, &error))
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        {
+          ide_buffer_set_state (self, IDE_BUFFER_STATE_FAILED);
+          ide_notification_set_progress (state->notif, 0.0);
+          ide_task_return_error (task, g_steal_pointer (&error));
+          IDE_EXIT;
+        }
+
+      g_clear_error (&error);
+    }
+
+  /* First move the insert cursor back to 0:0, plugins might move it
+   * but we certainly don't want to leave it at the end.
+   */
+  gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (self), &iter);
+  gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self), &iter, &iter);
+
+  ide_highlight_engine_unpause (self->highlight_engine);
+  ide_buffer_set_state (self, IDE_BUFFER_STATE_READY);
+  ide_notification_set_progress (state->notif, 1.0);
+  ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+void
+_ide_buffer_load_file_async (IdeBuffer            *self,
+                             GCancellable         *cancellable,
+                             IdeNotification     **notif,
+                             GAsyncReadyCallback   callback,
+                             gpointer              user_data)
+{
+  g_autoptr(GtkSourceFileLoader) loader = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  LoadState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (ide_buffer_get_file (self) != NULL);
+  ide_clear_param (notif, NULL);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_buffer_load_file_async);
+
+  if (self->state != IDE_BUFFER_STATE_READY &&
+      self->state != IDE_BUFFER_STATE_FAILED)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_BUSY,
+                                 "Cannot load file while buffer is busy");
+      IDE_EXIT;
+    }
+
+  state = g_slice_new0 (LoadState);
+  state->file = g_object_ref (ide_buffer_get_file (self));
+  state->notif = ide_notification_new ();
+  state->highlight_syntax = gtk_source_buffer_get_highlight_syntax (GTK_SOURCE_BUFFER (self));
+  ide_task_set_task_data (task, state, load_state_free);
+
+  ide_buffer_set_state (self, IDE_BUFFER_STATE_LOADING);
+
+  /* Disable some features while we reload */
+  gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (self), FALSE);
+  ide_highlight_engine_pause (self->highlight_engine);
+
+  loader = gtk_source_file_loader_new (GTK_SOURCE_BUFFER (self), self->source_file);
+  gtk_source_file_loader_load_async (loader,
+                                     G_PRIORITY_DEFAULT,
+                                     cancellable,
+                                     ide_buffer_progress_cb,
+                                     g_object_ref (state->notif),
+                                     g_object_unref,
+                                     ide_buffer_load_file_cb,
+                                     g_steal_pointer (&task));
+
+  /* Load file settings immediately so that we can increase the chance
+   * they are settled by the the load operation is finished. The modelines
+   * file settings will auto-monitor for IdeBufferManager::buffer-loaded
+   * and settle the file settings when we complete.
+   */
+  ide_buffer_reload_file_settings (self);
+
+  if (notif != NULL)
+    *notif = g_object_ref (state->notif);
+
+  IDE_EXIT;
+}
+
+/**
+ * _ide_buffer_load_file_finish:
+ * @self: an #IdeBuffer
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * This should be called by the buffer manager to complete loading the initial
+ * state of a buffer. It can also be used to reload a buffer after it was
+ * modified on disk.
+ *
+ * You MUST call this function after using _ide_buffer_load_file_async() so
+ * that the completion of signals and addins may be notified.
+ *
+ * Returns: %TRUE if the file was successfully loaded
+ *
+ * Since: 3.32
+ */
+gboolean
+_ide_buffer_load_file_finish (IdeBuffer     *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  LoadState *state;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  if (!ide_task_propagate_boolean (IDE_TASK (result), error))
+    return FALSE;
+
+  /* Restore various buffer features we disabled while loading */
+  state = ide_task_get_task_data (IDE_TASK (result));
+  if (state->highlight_syntax)
+    gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (self), TRUE);
+
+  /* Let consumers know they can access the buffer now */
+  g_signal_emit (self, signals [LOADED], 0);
+
+  /* Notify buffer addins that a file has been loaded */
+  if (self->addins != NULL)
+    {
+      IdeBufferFileLoad closure = { self, state->file };
+      ide_extension_set_adapter_foreach (self->addins,
+                                         _ide_buffer_addin_file_loaded_cb,
+                                         &closure);
+    }
+
+  return TRUE;
+}
+
+static void
+ide_buffer_save_file_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  GtkSourceFileSaver *saver = (GtkSourceFileSaver *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBuffer *self;
+  SaveState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_SOURCE_IS_FILE_SAVER (saver));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (state != NULL);
+  g_assert (G_IS_FILE (state->file));
+  g_assert (IDE_IS_NOTIFICATION (state->notif));
+
+  if (!gtk_source_file_saver_save_finish (saver, result, &error))
+    {
+      ide_notification_set_progress (state->notif, 0.0);
+      ide_buffer_set_state (self, IDE_BUFFER_STATE_FAILED);
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  ide_notification_set_progress (state->notif, 1.0);
+  ide_buffer_set_state (self, IDE_BUFFER_STATE_READY);
+
+  /* Notify addins that a save has completed */
+  if (self->addins != NULL)
+    {
+      IdeBufferFileSave closure = { self, state->file };
+      ide_extension_set_adapter_foreach (self->addins,
+                                         _ide_buffer_addin_file_saved_cb,
+                                         &closure);
+    }
+
+  if (self->buffer_manager)
+    _ide_buffer_manager_buffer_saved (self->buffer_manager, self);
+
+  ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_save_file_async:
+ * @self: an #IdeBuffer
+ * @file: (nullable): a #GFile or %NULL
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously saves the buffer contents to @file.
+ *
+ * If @file is %NULL, then the #IdeBuffer:file property is used.
+ *
+ * The buffer is marked as busy during the operation, and must not have
+ * further editing until the operation is complete.
+ *
+ * @callback is executed upon completion and should call
+ * ide_buffer_save_file_finish() to get the result of the operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_save_file_async (IdeBuffer            *self,
+                            GFile                *file,
+                            GCancellable         *cancellable,
+                            IdeNotification     **notif,
+                            GAsyncReadyCallback   callback,
+                            gpointer              user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GtkSourceFile) alternate = NULL;
+  g_autoptr(GtkSourceFileSaver) saver = NULL;
+  g_autoptr(IdeNotification) local_notif = NULL;
+  GtkSourceFile *source_file;
+  SaveState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (!file || G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  ide_clear_param (notif, NULL);
+
+  /* If the user is requesting to save a file and our current file
+   * is a temporary file, then we want to transition to become that
+   * file instead of our temporary one.
+   */
+  if (file != NULL && self->is_temporary)
+    {
+      _ide_buffer_set_file (self, file);
+      self->is_temporary = FALSE;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_TEMPORARY]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+
+  if (file == NULL)
+    file = ide_buffer_get_file (self);
+
+  local_notif = ide_notification_new ();
+  ide_notification_set_has_progress (local_notif, TRUE);
+
+  state = g_slice_new0 (SaveState);
+  state->file = g_object_ref (file);
+  state->notif = g_object_ref (local_notif);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_save_file_async);
+  ide_task_set_task_data (task, state, save_state_free);
+
+  if (self->state != IDE_BUFFER_STATE_READY)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_BUSY,
+                                 "Failed to save buffer as it is busy");
+      IDE_EXIT;
+    }
+
+  source_file = self->source_file;
+
+  if (file && !g_file_equal (file, ide_buffer_get_file (self)))
+    {
+      alternate = gtk_source_file_new ();
+      gtk_source_file_set_location (alternate, file);
+      source_file = alternate;
+    }
+
+  if (self->addins != NULL)
+    {
+      IdeBufferFileSave closure = { self, file };
+      ide_extension_set_adapter_foreach (self->addins,
+                                         _ide_buffer_addin_save_file_cb,
+                                         &closure);
+    }
+
+  saver = gtk_source_file_saver_new (GTK_SOURCE_BUFFER (self), source_file);
+  ide_buffer_set_state (self, IDE_BUFFER_STATE_SAVING);
+  gtk_source_file_saver_save_async (saver,
+                                    G_PRIORITY_DEFAULT,
+                                    cancellable,
+                                    ide_buffer_progress_cb,
+                                    g_object_ref (local_notif),
+                                    g_object_unref,
+                                    ide_buffer_save_file_cb,
+                                    g_steal_pointer (&task));
+
+  if (notif != NULL)
+    *notif = g_steal_pointer (&local_notif);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_save_file_finish:
+ * @self: an #IdeBuffer
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to save the buffer via
+ * ide_buffer_save_file_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_save_file_finish (IdeBuffer     *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_buffer_get_language_id:
+ * @self: an #IdeBuffer
+ *
+ * A helper to get the language identifier of the buffers current language.
+ *
+ * Returns: (nullable): a string containing the language id, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_buffer_get_language_id (IdeBuffer *self)
+{
+  GtkSourceLanguage *lang;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if ((lang = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self))))
+    return gtk_source_language_get_id (lang);
+
+  return NULL;
+}
+
+void
+ide_buffer_set_language_id (IdeBuffer   *self,
+                            const gchar *language_id)
+{
+  GtkSourceLanguage *language = NULL;
+
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  if (language_id != NULL)
+    {
+      GtkSourceLanguageManager *manager;
+
+      manager = gtk_source_language_manager_get_default ();
+      language = gtk_source_language_manager_get_language (manager, language_id);
+    }
+
+  gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (self), language);
+}
+
+IdeHighlightEngine *
+_ide_buffer_get_highlight_engine (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->highlight_engine;
+}
+
+void
+_ide_buffer_set_failure (IdeBuffer    *self,
+                         const GError *error)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  if (error == self->failure)
+    return;
+
+  if (error != NULL)
+    self->state = IDE_BUFFER_STATE_FAILED;
+
+  g_clear_pointer (&self->failure, g_error_free);
+  self->failure = g_error_copy (error);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FAILED]);
+}
+
+/**
+ * ide_buffer_get_failure:
+ *
+ * Gets a #GError representing a failure that has occurred for the
+ * buffer.
+ *
+ * Returns: (transfer none): a #GError, or %NULL
+ *
+ * Since: 3.32
+ */
+const GError *
+ide_buffer_get_failure (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->failure;
+}
+
+/**
+ * ide_buffer_get_failed:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:failed property, denoting if the buffer has failed
+ * in some aspect such as loading or saving.
+ *
+ * Returns: %TRUE if the buffer is in a failed state
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_failed (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->state == IDE_BUFFER_STATE_FAILED;
+}
+
+static void
+ide_buffer_set_file_settings (IdeBuffer       *self,
+                              IdeFileSettings *file_settings)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (self->file_settings == file_settings)
+    return;
+
+  ide_clear_and_destroy_object (&self->file_settings);
+  self->file_settings = g_object_ref (file_settings);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE_SETTINGS]);
+}
+
+static void
+ide_buffer_reload_file_settings (IdeBuffer *self)
+{
+  IdeObjectBox *box;
+  const gchar *lang_id;
+  GFile *file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  file = ide_buffer_get_file (self);
+  lang_id = ide_buffer_get_language_id (self);
+
+  /* Bail if we'll just create the same settings as before */
+  if (self->file_settings != NULL &&
+      (g_file_equal (file, ide_file_settings_get_file (self->file_settings)) &&
+       ide_str_equal0 (lang_id, ide_file_settings_get_language (self->file_settings))))
+    return;
+
+  /* Now apply the settings (and they'll settle in the background) */
+  if ((box = ide_object_box_from_object (G_OBJECT (self))))
+    {
+      g_autoptr(IdeFileSettings) file_settings = NULL;
+
+      file_settings = ide_file_settings_new (IDE_OBJECT (box), file, lang_id);
+      ide_buffer_set_file_settings (self, file_settings);
+    }
+}
+
+static void
+ide_buffer_emit_cursor_moved (IdeBuffer *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (!ide_buffer_get_loading (self))
+    {
+      GtkTextMark *mark;
+      GtkTextIter iter;
+
+      mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (self));
+      gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (self), &iter, mark);
+      g_signal_emit (self, signals [CURSOR_MOVED], 0, &iter);
+    }
+}
+
+/**
+ * ide_buffer_get_loading:
+ * @self: an #IdeBuffer
+ *
+ * This checks to see if the buffer is currently loading. This is equivalent
+ * to calling ide_buffer_get_state() and checking for %IDE_BUFFER_STATE_LOADING.
+ *
+ * Returns: %TRUE if the buffer is loading; otherwise %FALSE.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_loading (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return ide_buffer_get_state (self) == IDE_BUFFER_STATE_LOADING;
+}
+
+static void
+ide_buffer_changed (GtkTextBuffer *buffer)
+{
+  IdeBuffer *self = (IdeBuffer *)buffer;
+
+  g_assert (IDE_IS_BUFFER (self));
+
+  GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->changed (buffer);
+
+  self->change_count++;
+  g_clear_pointer (&self->content, g_bytes_unref);
+  ide_buffer_delay_settling (self);
+}
+
+static void
+ide_buffer_delete_range (GtkTextBuffer *buffer,
+                         GtkTextIter   *begin,
+                         GtkTextIter   *end)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    gint begin_line, begin_offset;
+    gint end_line, end_offset;
+
+    begin_line = gtk_text_iter_get_line (begin);
+    begin_offset = gtk_text_iter_get_line_offset (begin);
+    end_line = gtk_text_iter_get_line (end);
+    end_offset = gtk_text_iter_get_line_offset (end);
+
+    IDE_TRACE_MSG ("delete-range (%d:%d, %d:%d)",
+                   begin_line, begin_offset,
+                   end_line, end_offset);
+  }
+#endif
+
+  GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->delete_range (buffer, begin, end);
+
+  ide_buffer_emit_cursor_moved (IDE_BUFFER (buffer));
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_insert_text (GtkTextBuffer *buffer,
+                        GtkTextIter   *location,
+                        const gchar   *text,
+                        gint           len)
+{
+  gboolean recheck_language = FALSE;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (location != NULL);
+  g_assert (text != NULL);
+
+  /*
+   * If we are inserting a \n at the end of the first line, then we might want
+   * to adjust the GtkSourceBuffer:language property to reflect the format.
+   * This is similar to emacs "modelines", which is apparently a bit of an
+   * overloaded term as is not to be confused with editor setting modelines.
+   */
+  if ((gtk_text_iter_get_line (location) == 0) && gtk_text_iter_ends_line (location) &&
+      ((text [0] == '\n') || ((len > 1) && (strchr (text, '\n') != NULL))))
+    recheck_language = TRUE;
+
+  GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->insert_text (buffer, location, text, len);
+
+  ide_buffer_emit_cursor_moved (IDE_BUFFER (buffer));
+
+  if (recheck_language)
+    ide_buffer_guess_language (IDE_BUFFER (buffer));
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_mark_set (GtkTextBuffer     *buffer,
+                     const GtkTextIter *iter,
+                     GtkTextMark       *mark)
+{
+  IdeBuffer *self = (IdeBuffer *)buffer;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->mark_set (buffer, iter, mark);
+
+  if (!ide_buffer_get_loading (self))
+    {
+      if (mark == gtk_text_buffer_get_insert (buffer))
+        ide_buffer_emit_cursor_moved (IDE_BUFFER (buffer));
+    }
+}
+
+/**
+ * ide_buffer_get_changed_on_volume:
+ * @self: an #IdeBuffer
+ *
+ * Returns %TRUE if the #IdeBuffer is known to have been modified on storage
+ * externally from this #IdeBuffer.
+ *
+ * Returns: %TRUE if @self is known to be modified on storage
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_changed_on_volume (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->changed_on_volume;
+}
+
+/**
+ * _ide_buffer_set_changed_on_volume:
+ * @self: an #IdeBuffer
+ * @changed_on_volume: if the buffer was changed externally
+ *
+ * Sets the #IdeBuffer:changed-on-volume property.
+ *
+ * Set this to %TRUE if the buffer has been discovered to have changed
+ * outside of this buffer.
+ *
+ * Since: 3.32
+ */
+void
+_ide_buffer_set_changed_on_volume (IdeBuffer *self,
+                                   gboolean   changed_on_volume)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  changed_on_volume = !!changed_on_volume;
+
+  if (changed_on_volume != self->changed_on_volume)
+    {
+      self->changed_on_volume = changed_on_volume;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHANGED_ON_VOLUME]);
+    }
+}
+
+/**
+ * ide_buffer_get_read_only:
+ *
+ * This function returns %TRUE if the underlying file has been discovered to
+ * be read-only. This may be used by the interface to display information to
+ * the user about saving the file.
+ *
+ * Returns: %TRUE if the underlying file is read-only
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_read_only (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->read_only;
+}
+
+/**
+ * _ide_buffer_set_read_only:
+ * @self: an #IdeBuffer
+ * @read_only: if the buffer is read-only
+ *
+ * Sets the #IdeBuffer:read-only property, which should be set when the buffer
+ * has been discovered to be read-only on disk.
+ *
+ * Since: 3.32
+ */
+void
+_ide_buffer_set_read_only (IdeBuffer *self,
+                           gboolean   read_only)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  read_only = !!read_only;
+
+  if (read_only != self->read_only)
+    {
+      self->read_only = read_only;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_READ_ONLY]);
+    }
+}
+
+/**
+ * ide_buffer_get_style_scheme_name:
+ * @self: an #IdeBuffer
+ *
+ * Gets the name of the #GtkSourceStyleScheme from the #IdeBuffer:style-scheme
+ * property.
+ *
+ * Returns: (nullable): a string containing the style scheme or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_buffer_get_style_scheme_name (IdeBuffer *self)
+{
+  GtkSourceStyleScheme *scheme;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if ((scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (self))))
+    return gtk_source_style_scheme_get_id (scheme);
+
+  return NULL;
+}
+
+/**
+ * ide_buffer_set_style_scheme_name:
+ * @self: an #IdeBuffer
+ * @style_scheme_name: (nullable): string containing the style scheme's name
+ *
+ * Sets the #IdeBuffer:style-scheme property by locating the style scheme
+ * matching @style_scheme_name.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_set_style_scheme_name (IdeBuffer   *self,
+                                  const gchar *style_scheme_name)
+{
+  GtkSourceStyleSchemeManager *manager;
+  GtkSourceStyleScheme *scheme;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  if ((manager = gtk_source_style_scheme_manager_get_default ()) &&
+      (scheme = gtk_source_style_scheme_manager_get_scheme (manager, style_scheme_name)))
+    gtk_source_buffer_set_style_scheme (GTK_SOURCE_BUFFER (self), scheme);
+  else
+    gtk_source_buffer_set_style_scheme (GTK_SOURCE_BUFFER (self), NULL);
+}
+
+/**
+ * ide_buffer_get_title:
+ * @self: an #IdeBuffer
+ *
+ * Gets a string to represent the title of the buffer. An attempt is made to
+ * make this relative to the project workdir if possible.
+ *
+ * Returns: (transfer full): a string containing a title
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_buffer_dup_title (IdeBuffer *self)
+{
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) home = NULL;
+  GFile *file;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  file = ide_buffer_get_file (self);
+
+  if (self->is_temporary)
+    return g_file_get_basename (file);
+
+  /* Unlikely, but better to be safe */
+  if (!(context = ide_buffer_ref_context (self)))
+    return g_file_get_basename (file);
+
+  workdir = ide_context_ref_workdir (context);
+
+  if (g_file_has_prefix (file, workdir))
+    return g_file_get_relative_path (workdir, file);
+
+  home = g_file_new_for_path (g_get_home_dir ());
+
+  if (g_file_has_prefix (file, home))
+    {
+      g_autofree gchar *relative = g_file_get_relative_path (home, file);
+      return g_strdup_printf ("~/%s", relative);
+    }
+
+  if (!g_file_is_native (file))
+    return g_file_get_uri (file);
+  else
+    return g_file_get_path (file);
+}
+
+/**
+ * ide_buffer_get_highlight_diagnostics:
+ * @self: an #IdeBuffer
+ *
+ * Checks if diagnostics should be highlighted.
+ *
+ * Returns: %TRUE if diagnostics should be highlighted
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_highlight_diagnostics (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->highlight_diagnostics;
+}
+
+/**
+ * ide_buffer_set_highlight_diagnostics:
+ * @self: an #IdeBuffer
+ * @highlight_diagnostics: if diagnostics should be highlighted
+ *
+ * Sets the #IdeBuffer:highlight-diagnostics property.
+ *
+ * If set to %TRUE, diagnostics will be styled in the buffer.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_set_highlight_diagnostics (IdeBuffer *self,
+                                      gboolean   highlight_diagnostics)
+{
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  highlight_diagnostics = !!highlight_diagnostics;
+
+  if (self->highlight_diagnostics != highlight_diagnostics)
+    {
+      ide_buffer_clear_diagnostics (self);
+      self->highlight_diagnostics = highlight_diagnostics;
+      ide_buffer_apply_diagnostics (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HIGHLIGHT_DIAGNOSTICS]);
+    }
+}
+
+/**
+ * ide_buffer_get_iter_location:
+ * @self: an #IdeBuffer
+ * @iter: a #GtkTextIter
+ *
+ * Gets an #IdeLocation for the position represented by @iter.
+ *
+ * Returns: (transfer full): an #IdeLocation
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_buffer_get_iter_location (IdeBuffer         *self,
+                              const GtkTextIter *iter)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+  g_return_val_if_fail (iter != NULL, NULL);
+
+  return ide_location_new_with_offset (ide_buffer_get_file (self),
+                                       gtk_text_iter_get_line (iter),
+                                       gtk_text_iter_get_line_offset (iter),
+                                       gtk_text_iter_get_offset (iter));
+}
+
+/**
+ * ide_buffer_get_selection_range:
+ * @self: an #IdeBuffer
+ *
+ * Gets an #IdeRange to represent the current buffer selection.
+ *
+ * Returns: (transfer full): an #IdeRange
+ *
+ * Since: 3.32
+ */
+IdeRange *
+ide_buffer_get_selection_range (IdeBuffer *self)
+{
+  g_autoptr(IdeLocation) begin = NULL;
+  g_autoptr(IdeLocation) end = NULL;
+  GtkTextIter begin_iter;
+  GtkTextIter end_iter;
+
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self), &begin_iter, &end_iter);
+  gtk_text_iter_order (&begin_iter, &end_iter);
+
+  begin = ide_buffer_get_iter_location (self, &begin_iter);
+  end = ide_buffer_get_iter_location (self, &end_iter);
+
+  return ide_range_new (begin, end);
+}
+
+/**
+ * ide_buffer_get_change_count:
+ * @self: an #IdeBuffer
+ *
+ * Gets the monotonic change count for the buffer.
+ *
+ * Returns: the change count for the buffer
+ *
+ * Since: 3.32
+ */
+guint
+ide_buffer_get_change_count (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), 0);
+
+  return self->change_count;
+}
+
+static gboolean
+ide_buffer_settled_cb (gpointer user_data)
+{
+  IdeBuffer *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  self->settling_source = 0;
+  g_signal_emit (self, signals [CHANGE_SETTLED], 0);
+
+  if (self->addins != NULL)
+    ide_extension_set_adapter_foreach (self->addins,
+                                       _ide_buffer_addin_change_settled_cb,
+                                       self);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_buffer_delay_settling (IdeBuffer *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  g_clear_handle_id (&self->settling_source, g_source_remove);
+  self->settling_source = gdk_threads_add_timeout (SETTLING_DELAY_MSEC,
+                                                   ide_buffer_settled_cb,
+                                                   self);
+}
+
+/**
+ * ide_buffer_set_diagnostics:
+ * @self: an #IdeBuffer
+ * @diagnostics: (nullable): an #IdeDiagnostics
+ *
+ * Sets the #IdeDiagnostics for the buffer. These will be used to highlight
+ * the buffer for errors and warnings if #IdeBuffer:highlight-diagnostics
+ * is %TRUE.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_set_diagnostics (IdeBuffer      *self,
+                            IdeDiagnostics *diagnostics)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (!diagnostics || IDE_IS_DIAGNOSTICS (diagnostics));
+
+  if (diagnostics == self->diagnostics)
+    return;
+
+  if (self->diagnostics)
+    {
+      ide_buffer_clear_diagnostics (self);
+      g_clear_object (&self->diagnostics);
+    }
+
+  if (diagnostics)
+    {
+      self->diagnostics = g_object_ref (diagnostics);
+      ide_buffer_apply_diagnostics (self);
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DIAGNOSTICS]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+
+  _ide_buffer_line_flags_changed (self);
+}
+
+/**
+ * ide_buffer_get_diagnostics:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeDiagnostics for the buffer if any have been registered.
+ *
+ * Returns: (transfer none) (nullable): an #IdeDiagnostics or %NULL
+ *
+ * Since: 3.32
+ */
+IdeDiagnostics *
+ide_buffer_get_diagnostics (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->diagnostics;
+}
+
+/**
+ * ide_buffer_has_diagnostics:
+ * @self: a #IdeBuffer
+ *
+ * Returns %TRUE if any diagnostics have been registered for the buffer.
+ *
+ * Returns: %TRUE if there are a non-zero number of diagnostics.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_has_diagnostics (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  if (self->diagnostics)
+    return g_list_model_get_n_items (G_LIST_MODEL (self->diagnostics)) > 0;
+
+  return FALSE;
+}
+
+static void
+ide_buffer_clear_diagnostics (IdeBuffer *self)
+{
+  GtkTextTagTable *table;
+  GtkTextTag *tag;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (!self->highlight_diagnostics)
+    return;
+
+  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self), &begin, &end);
+
+  table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (self));
+
+  if (NULL != (tag = gtk_text_tag_table_lookup (table, TAG_NOTE)))
+    dzl_gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self), tag, &begin, &end, TRUE);
+
+  if (NULL != (tag = gtk_text_tag_table_lookup (table, TAG_WARNING)))
+    dzl_gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self), tag, &begin, &end, TRUE);
+
+  if (NULL != (tag = gtk_text_tag_table_lookup (table, TAG_DEPRECATED)))
+    dzl_gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self), tag, &begin, &end, TRUE);
+
+  if (NULL != (tag = gtk_text_tag_table_lookup (table, TAG_ERROR)))
+    dzl_gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self), tag, &begin, &end, TRUE);
+}
+
+static void
+ide_buffer_apply_diagnostic (IdeBuffer     *self,
+                             IdeDiagnostic *diagnostic)
+{
+  IdeDiagnosticSeverity severity;
+  const gchar *tag_name = NULL;
+  IdeLocation *location;
+  guint n_ranges;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (IDE_IS_DIAGNOSTIC (diagnostic));
+
+  severity = ide_diagnostic_get_severity (diagnostic);
+
+  switch (severity)
+    {
+    case IDE_DIAGNOSTIC_NOTE:
+      tag_name = TAG_NOTE;
+      break;
+
+    case IDE_DIAGNOSTIC_DEPRECATED:
+      tag_name = TAG_DEPRECATED;
+      break;
+
+    case IDE_DIAGNOSTIC_WARNING:
+      tag_name = TAG_WARNING;
+      break;
+
+    case IDE_DIAGNOSTIC_ERROR:
+    case IDE_DIAGNOSTIC_FATAL:
+      tag_name = TAG_ERROR;
+      break;
+
+    case IDE_DIAGNOSTIC_IGNORED:
+    default:
+      return;
+    }
+
+  if ((location = ide_diagnostic_get_location (diagnostic)))
+    {
+      GtkTextIter begin_iter;
+      GtkTextIter end_iter;
+
+      ide_buffer_get_iter_at_location (self, &begin_iter, location);
+      end_iter = begin_iter;
+
+      if (!gtk_text_iter_ends_line (&end_iter))
+        gtk_text_iter_forward_to_line_end (&end_iter);
+      else
+        gtk_text_iter_backward_char (&begin_iter);
+
+      gtk_text_buffer_apply_tag_by_name (GTK_TEXT_BUFFER (self), tag_name, &begin_iter, &end_iter);
+    }
+
+  n_ranges = ide_diagnostic_get_n_ranges (diagnostic);
+
+  for (guint i = 0; i < n_ranges; i++)
+    {
+      GtkTextIter begin_iter;
+      GtkTextIter end_iter;
+      IdeLocation *begin;
+      IdeLocation *end;
+      IdeRange *range;
+      GFile *file;
+
+      range = ide_diagnostic_get_range (diagnostic, i);
+      begin = ide_range_get_begin (range);
+      end = ide_range_get_end (range);
+      file = ide_location_get_file (begin);
+
+      if (file != NULL)
+        {
+          if (!g_file_equal (file, ide_buffer_get_file (self)))
+            continue;
+        }
+
+      ide_buffer_get_iter_at_location (self, &begin_iter, begin);
+      ide_buffer_get_iter_at_location (self, &end_iter, end);
+
+      if (gtk_text_iter_equal (&begin_iter, &end_iter))
+        {
+          if (!gtk_text_iter_ends_line (&end_iter))
+            gtk_text_iter_forward_char (&end_iter);
+          else
+            gtk_text_iter_backward_char (&begin_iter);
+        }
+
+      gtk_text_buffer_apply_tag_by_name (GTK_TEXT_BUFFER (self), tag_name, &begin_iter, &end_iter);
+    }
+}
+
+static void
+ide_buffer_apply_diagnostics (IdeBuffer *self)
+{
+  guint n_items;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (!self->highlight_diagnostics)
+    return;
+
+  if (self->diagnostics == NULL)
+    return;
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->diagnostics));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeDiagnostic) diagnostic = NULL;
+
+      diagnostic = g_list_model_get_item (G_LIST_MODEL (self->diagnostics), i);
+      ide_buffer_apply_diagnostic (self, diagnostic);
+    }
+}
+
+/**
+ * ide_buffer_get_iter_at_location:
+ * @self: an #IdeBuffer
+ * @iter: (out): a #GtkTextIter
+ * @location: a #IdeLocation
+ *
+ * Set @iter to the position designated by @location.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_get_iter_at_location (IdeBuffer   *self,
+                                 GtkTextIter *iter,
+                                 IdeLocation *location)
+{
+  gint line;
+  gint line_offset;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (location != NULL);
+
+  line = ide_location_get_line (location);
+  line_offset = ide_location_get_line_offset (location);
+
+  gtk_text_buffer_get_iter_at_line_offset (GTK_TEXT_BUFFER (self),
+                                           iter,
+                                           MAX (0, line),
+                                           MAX (0, line_offset));
+
+  /* Advance to first non-space if offset < 0 */
+  if (line_offset < 0)
+    {
+      while (!gtk_text_iter_ends_line (iter))
+        {
+          if (!g_unichar_isspace (gtk_text_iter_get_char (iter)))
+            break;
+          gtk_text_iter_forward_char (iter);
+        }
+    }
+}
+
+/**
+ * ide_buffer_get_change_monitor:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:change-monitor for the buffer.
+ *
+ * Returns: (transfer none) (nullable): an #IdeBufferChangeMonitor or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBufferChangeMonitor *
+ide_buffer_get_change_monitor (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->change_monitor;
+}
+
+/**
+ * ide_buffer_set_change_monitor:
+ * @self: an #IdeBuffer
+ * @change_monitor: (nullable): an #IdeBufferChangeMonitor or %NULL
+ *
+ * Sets an #IdeBufferChangeMonitor to use for the buffer.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_set_change_monitor (IdeBuffer              *self,
+                               IdeBufferChangeMonitor *change_monitor)
+{
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (!change_monitor || IDE_IS_BUFFER_CHANGE_MONITOR (change_monitor));
+
+  if (g_set_object (&self->change_monitor, change_monitor))
+    {
+      /* Destroy change monitor with us if we can */
+      if (change_monitor && ide_object_is_root (IDE_OBJECT (change_monitor)))
+        {
+          IdeObjectBox *box = ide_object_box_from_object (G_OBJECT (self));
+          ide_object_append (IDE_OBJECT (box), IDE_OBJECT (change_monitor));
+        }
+
+      if (change_monitor != NULL)
+        ide_buffer_change_monitor_reload (change_monitor);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHANGE_MONITOR]);
+    }
+}
+
+static gboolean
+ide_buffer_can_do_newline_hack (IdeBuffer *self,
+                                guint      len)
+{
+  guint next_pow2;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  /*
+   * If adding two bytes to our length (one for \n and one for \0) is still
+   * under the next power of two, then we can avoid making a copy of the buffer
+   * when saving the buffer to our drafts.
+   *
+   * HACK: This relies on the fact that GtkTextBuffer returns a GString
+   *       allocated string which grows the string in powers of two.
+   */
+
+  if ((len == 0) || (len & (len - 1)) == 0)
+    return FALSE;
+
+  next_pow2 = len;
+  next_pow2 |= next_pow2 >> 1;
+  next_pow2 |= next_pow2 >> 2;
+  next_pow2 |= next_pow2 >> 4;
+  next_pow2 |= next_pow2 >> 8;
+  next_pow2 |= next_pow2 >> 16;
+  next_pow2++;
+
+  return ((len + 2) < next_pow2);
+}
+
+/**
+ * ide_buffer_dup_content:
+ * @self: an #IdeBuffer.
+ *
+ * Gets the contents of the buffer as GBytes.
+ *
+ * By using this function to get the bytes, you allow #IdeBuffer to avoid
+ * calculating the buffer text unnecessarily, potentially saving on
+ * allocations.
+ *
+ * Additionally, this allows the buffer to update the state in #IdeUnsavedFiles
+ * if the content is out of sync.
+ *
+ * Returns: (transfer full): a #GBytes containing the buffer content.
+ *
+ * Since: 3.32
+ */
+GBytes *
+ide_buffer_dup_content (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if (self->content == NULL)
+    {
+      g_autoptr(IdeContext) context = NULL;
+      IdeUnsavedFiles *unsaved_files;
+      GtkTextIter begin;
+      GtkTextIter end;
+      GFile *file;
+      gchar *text;
+      gsize len;
+
+      gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self), &begin, &end);
+      text = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (self), &begin, &end, TRUE);
+
+      /*
+       * If implicit newline is set, add a \n in place of the \0 and avoid
+       * duplicating the buffer. Make sure to track length beforehand, since we
+       * would overwrite afterwards. Since conversion to \r\n is dealth with
+       * during save operations, this should be fine for both. The unsaved
+       * files will restore to a buffer, for which \n is acceptable.
+       */
+      len = strlen (text);
+      if (gtk_source_buffer_get_implicit_trailing_newline (GTK_SOURCE_BUFFER (self)) &&
+          (len == 0 || text[len - 1] != '\n'))
+        {
+          if (!ide_buffer_can_do_newline_hack (self, len))
+            {
+              gchar *copy;
+
+              copy = g_malloc (len + 2);
+              memcpy (copy, text, len);
+              g_free (text);
+              text = copy;
+            }
+
+          text [len] = '\n';
+          text [++len] = '\0';
+        }
+
+      /*
+       * We pass a buffer that is longer than the length we tell GBytes about.
+       * This way, compilers that don't want to see the trailing \0 can ignore
+       * that data, but compilers that rely on valid C strings can also rely
+       * on the buffer to be valid.
+       */
+      self->content = g_bytes_new_take (g_steal_pointer (&text), len);
+
+      file = ide_buffer_get_file (self);
+      context = ide_buffer_ref_context (IDE_BUFFER (self));
+      unsaved_files = ide_unsaved_files_from_context (context);
+      ide_unsaved_files_update (unsaved_files, file, self->content);
+    }
+
+  return g_bytes_ref (self->content);
+}
+
+static void
+ide_buffer_format_selection_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeFormatter *formatter = (IdeFormatter *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+
+  g_assert (IDE_IS_FORMATTER (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_formatter_format_finish (formatter, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_buffer_format_selection_range_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeFormatter *formatter = (IdeFormatter *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+
+  g_assert (IDE_IS_FORMATTER (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_formatter_format_range_finish (formatter, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_buffer_format_selection_async:
+ * @self: an #IdeBuffer
+ * @options: options for the formatting
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @callback: the callback upon completion
+ * @user_data: user data for @callback
+ *
+ * Formats the selection using an available #IdeFormatter for the buffer.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_format_selection_async (IdeBuffer           *self,
+                                   IdeFormatterOptions *options,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeFormatter *formatter;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (IDE_IS_FORMATTER_OPTIONS (options));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_format_selection_async);
+
+  if (!(formatter = ide_extension_adapter_get_extension (self->formatter)))
+    {
+      const gchar *language_id = ide_buffer_get_language_id (self);
+
+      if (language_id == NULL)
+        language_id = "none";
+
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "No formatter registered for language %s",
+                                 language_id);
+
+      IDE_EXIT;
+    }
+
+  if (!gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self), &begin, &end))
+    {
+      ide_formatter_format_async (formatter,
+                                  self,
+                                  options,
+                                  cancellable,
+                                  ide_buffer_format_selection_cb,
+                                  g_steal_pointer (&task));
+      IDE_EXIT;
+    }
+
+  gtk_text_iter_order (&begin, &end);
+
+  ide_formatter_format_range_async (formatter,
+                                    self,
+                                    options,
+                                    &begin,
+                                    &end,
+                                    cancellable,
+                                    ide_buffer_format_selection_range_cb,
+                                    g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_format_selection_finish:
+ * @self: an #IdeBuffer
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_buffer_format_selection_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_format_selection_finish (IdeBuffer     *self,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_buffer_get_insert_location:
+ *
+ * Gets the location of the insert mark as an #IdeLocation.
+ *
+ * Returns: (transfer full): An #IdeLocation
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_buffer_get_insert_location (IdeBuffer *self)
+{
+  GtkTextMark *mark;
+  GtkTextIter iter;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (self));
+  gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (self), &iter, mark);
+
+  return ide_buffer_get_iter_location (self, &iter);
+}
+
+/**
+ * ide_buffer_get_word_at_iter:
+ * @self: an #IdeBuffer.
+ * @iter: a #GtkTextIter.
+ *
+ * Gets the word found under the position denoted by @iter.
+ *
+ * Returns: (transfer full): A newly allocated string.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_buffer_get_word_at_iter (IdeBuffer         *self,
+                             const GtkTextIter *iter)
+{
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+  g_return_val_if_fail (iter != NULL, NULL);
+
+  end = begin = *iter;
+
+  if (!_ide_source_iter_starts_word (&begin))
+    _ide_source_iter_backward_extra_natural_word_start (&begin);
+
+  if (!_ide_source_iter_ends_word (&end))
+    _ide_source_iter_forward_extra_natural_word_end (&end);
+
+  return gtk_text_iter_get_slice (&begin, &end);
+}
+
+/**
+ * ide_buffer_get_rename_provider:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeRenameProvider for this buffer, or %NULL.
+ *
+ * Returns: (nullable) (transfer none): An #IdeRenameProvider or %NULL if
+ *   there is no #IdeRenameProvider that can statisfy the buffer.
+ *
+ * Since: 3.32
+ */
+IdeRenameProvider *
+ide_buffer_get_rename_provider (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if (self->rename_provider != NULL)
+    return ide_extension_adapter_get_extension (self->rename_provider);
+
+  return NULL;
+}
+
+/**
+ * ide_buffer_get_file_settings:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:file-settings property.
+ *
+ * The #IdeFileSettings are updated when changes to the file or language
+ * syntax are chnaged.
+ *
+ * Returns: (transfer none) (nullable): an #IdeFileSettings or %NULL
+ *
+ * Since: 3.32
+ */
+IdeFileSettings *
+ide_buffer_get_file_settings (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->file_settings;
+}
+
+/**
+ * ide_buffer_ref_context:
+ * @self: an #IdeBuffer
+ *
+ * Locates the #IdeContext for the buffer and returns it.
+ *
+ * Returns: (transfer full): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_buffer_ref_context (IdeBuffer *self)
+{
+  g_autoptr(IdeObject) root = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if (self->buffer_manager != NULL)
+    root = ide_object_ref_root (IDE_OBJECT (self->buffer_manager));
+
+  g_return_val_if_fail (root != NULL, NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (root), NULL);
+
+  return IDE_CONTEXT (g_steal_pointer (&root));
+}
+
+static void
+apply_style (GtkTextTag  *tag,
+             const gchar *first_property,
+             ...)
+{
+  va_list args;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (!tag || GTK_IS_TEXT_TAG (tag));
+  g_assert (first_property != NULL);
+
+  if (tag == NULL)
+    return;
+
+  va_start (args, first_property);
+  g_object_set_valist (G_OBJECT (tag), first_property, args);
+  va_end (args);
+}
+
+static void
+ide_buffer_notify_style_scheme (IdeBuffer  *self,
+                                GParamSpec *pspec,
+                                gpointer    unused)
+{
+  GtkSourceStyleScheme *style_scheme;
+  GtkTextTagTable *table;
+  GdkRGBA deprecated_rgba;
+  GdkRGBA error_rgba;
+  GdkRGBA note_rgba;
+  GdkRGBA warning_rgba;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (pspec != NULL);
+
+  style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (self));
+  table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (self));
+
+#define GET_TAG(name) (gtk_text_tag_table_lookup(table, name))
+
+  if (style_scheme != NULL)
+    {
+      /* These are a fall-back if our style scheme isn't installed. */
+      gdk_rgba_parse (&deprecated_rgba, DEPRECATED_COLOR);
+      gdk_rgba_parse (&error_rgba, ERROR_COLOR);
+      gdk_rgba_parse (&note_rgba, NOTE_COLOR);
+      gdk_rgba_parse (&warning_rgba, WARNING_COLOR);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_DEPRECATED,
+                                                GET_TAG (TAG_DEPRECATED)))
+        apply_style (GET_TAG (TAG_DEPRECATED),
+                     "underline", PANGO_UNDERLINE_ERROR,
+                     "underline-rgba", &deprecated_rgba,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_ERROR,
+                                                GET_TAG (TAG_ERROR)))
+        apply_style (GET_TAG (TAG_ERROR),
+                     "underline", PANGO_UNDERLINE_ERROR,
+                     "underline-rgba", &error_rgba,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_NOTE,
+                                                GET_TAG (TAG_NOTE)))
+        apply_style (GET_TAG (TAG_NOTE),
+                     "underline", PANGO_UNDERLINE_ERROR,
+                     "underline-rgba", &note_rgba,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_WARNING,
+                                                GET_TAG (TAG_WARNING)))
+        apply_style (GET_TAG (TAG_WARNING),
+                     "underline", PANGO_UNDERLINE_ERROR,
+                     "underline-rgba", &warning_rgba,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_SNIPPET_TAB_STOP,
+                                                GET_TAG (TAG_SNIPPET_TAB_STOP)))
+        apply_style (GET_TAG (TAG_SNIPPET_TAB_STOP),
+                     "underline", PANGO_UNDERLINE_SINGLE,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_DEFINITION,
+                                                GET_TAG (TAG_DEFINITION)))
+        apply_style (GET_TAG (TAG_DEFINITION),
+                     "underline", PANGO_UNDERLINE_SINGLE,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_CURRENT_BKPT,
+                                                GET_TAG (TAG_CURRENT_BKPT)))
+        apply_style (GET_TAG (TAG_CURRENT_BKPT),
+                     "paragraph-background", CURRENT_BKPT_BG,
+                     "foreground", CURRENT_BKPT_FG,
+                     NULL);
+    }
+
+#undef GET_TAG
+
+  if (self->addins != NULL)
+    ide_extension_set_adapter_foreach (self->addins,
+                                       _ide_buffer_addin_style_scheme_changed_cb,
+                                       self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STYLE_SCHEME_NAME]);
+
+}
+
+static void
+ide_buffer_on_tag_added (IdeBuffer       *self,
+                         GtkTextTag      *tag,
+                         GtkTextTagTable *table)
+{
+  GtkTextTag *chunk_tag;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (GTK_IS_TEXT_TAG (tag));
+  g_assert (GTK_IS_TEXT_TAG_TABLE (table));
+
+  /* Adjust priority of our tab-stop tag. */
+  chunk_tag = gtk_text_tag_table_lookup (table, "snippet::tab-stop");
+  if (chunk_tag != NULL)
+    gtk_text_tag_set_priority (chunk_tag,
+                               gtk_text_tag_table_get_size (table) - 1);
+}
+
+static void
+ide_buffer_init_tags (IdeBuffer *self)
+{
+  GtkTextTagTable *tag_table;
+  GtkSourceStyleScheme *style_scheme;
+  g_autoptr(GtkTextTag) deprecated_tag = NULL;
+  g_autoptr(GtkTextTag) error_tag = NULL;
+  g_autoptr(GtkTextTag) note_tag = NULL;
+  g_autoptr(GtkTextTag) warning_tag = NULL;
+  GdkRGBA deprecated_rgba;
+  GdkRGBA error_rgba;
+  GdkRGBA note_rgba;
+  GdkRGBA warning_rgba;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  tag_table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (self));
+  style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (self));
+
+  /* These are fall-back if our style scheme isn't installed. */
+  gdk_rgba_parse (&deprecated_rgba, DEPRECATED_COLOR);
+  gdk_rgba_parse (&error_rgba, ERROR_COLOR);
+  gdk_rgba_parse (&note_rgba, NOTE_COLOR);
+  gdk_rgba_parse (&warning_rgba, WARNING_COLOR);
+
+  /*
+   * NOTE:
+   *
+   * The tag table assigns priority upon insert. Each successive insert
+   * is higher priority than the last.
+   */
+
+  deprecated_tag = gtk_text_tag_new (TAG_DEPRECATED);
+  error_tag = gtk_text_tag_new (TAG_ERROR);
+  note_tag = gtk_text_tag_new (TAG_NOTE);
+  warning_tag = gtk_text_tag_new (TAG_WARNING);
+
+  if (!ide_source_style_scheme_apply_style (style_scheme, TAG_DEPRECATED, deprecated_tag))
+    apply_style (deprecated_tag,
+                 "underline", PANGO_UNDERLINE_ERROR,
+                 "underline-rgba", &deprecated_rgba,
+                 NULL);
+
+  if (!ide_source_style_scheme_apply_style (style_scheme, TAG_ERROR, error_tag))
+    apply_style (error_tag,
+                 "underline", PANGO_UNDERLINE_ERROR,
+                 "underline-rgba", &error_rgba,
+                 NULL);
+
+  if (!ide_source_style_scheme_apply_style (style_scheme, TAG_NOTE, note_tag))
+    apply_style (note_tag,
+                 "underline", PANGO_UNDERLINE_ERROR,
+                 "underline-rgba", &note_rgba,
+                 NULL);
+
+  if (!ide_source_style_scheme_apply_style (style_scheme, TAG_NOTE, warning_tag))
+    apply_style (warning_tag,
+                 "underline", PANGO_UNDERLINE_ERROR,
+                 "underline-rgba", &warning_rgba,
+                 NULL);
+
+  gtk_text_tag_table_add (tag_table, deprecated_tag);
+  gtk_text_tag_table_add (tag_table, error_tag);
+  gtk_text_tag_table_add (tag_table, note_tag);
+  gtk_text_tag_table_add (tag_table, warning_tag);
+
+  gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (self), TAG_SNIPPET_TAB_STOP,
+                              NULL);
+  gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (self), TAG_DEFINITION,
+                              "underline", PANGO_UNDERLINE_SINGLE,
+                              NULL);
+  gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (self), TAG_CURRENT_BKPT,
+                              "paragraph-background", CURRENT_BKPT_BG,
+                              "foreground", CURRENT_BKPT_FG,
+                              NULL);
+
+  g_signal_connect_object (tag_table,
+                           "tag-added",
+                           G_CALLBACK (ide_buffer_on_tag_added),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+/**
+ * ide_buffer_get_formatter:
+ * @self: an #IdeBuffer
+ *
+ * Gets an #IdeFormatter for the buffer, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeFormatter or %NULL
+ *
+ * Since: 3.32
+ */
+IdeFormatter *
+ide_buffer_get_formatter (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if (self->formatter == NULL)
+    return NULL;
+
+  return ide_extension_adapter_get_extension (self->formatter);
+}
+
+void
+_ide_buffer_sync_to_unsaved_files (IdeBuffer *self)
+{
+  GBytes *content;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if ((content = ide_buffer_dup_content (self)))
+    g_bytes_unref (content);
+}
+
+/**
+ * ide_buffer_rehighlight:
+ * @self: an #IdeBuffer
+ *
+ * Force @self to rebuild the highlighted words.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_rehighlight (IdeBuffer *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  /* In case we are disposing */
+  if (self->highlight_engine == NULL || ide_buffer_get_loading (self))
+    IDE_EXIT;
+
+  if (gtk_source_buffer_get_highlight_syntax (GTK_SOURCE_BUFFER (self)))
+    ide_highlight_engine_rebuild (self->highlight_engine);
+  else
+    ide_highlight_engine_clear (self->highlight_engine);
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_get_symbol_at_location_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeSymbolResolver *symbol_resolver = (IdeSymbolResolver *)object;
+  g_autoptr(IdeSymbol) symbol = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  LookUpSymbolData *data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SYMBOL_RESOLVER (symbol_resolver));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  data = ide_task_get_task_data (task);
+  g_assert (data->resolvers != NULL);
+  g_assert (data->resolvers->len > 0);
+
+  if ((symbol = ide_symbol_resolver_lookup_symbol_finish (symbol_resolver, result, &error)))
+    {
+      /*
+       * Store symbol which has definition location. If no symbol has
+       * definition location then store symbol which has declaration location.
+       */
+      if ((data->symbol == NULL) ||
+          (ide_symbol_get_location (symbol) != NULL) ||
+          (ide_symbol_get_location (data->symbol) == NULL &&
+           ide_symbol_get_header_location (symbol)))
+        {
+          g_clear_object (&data->symbol);
+          data->symbol = g_steal_pointer (&symbol);
+        }
+    }
+
+  g_ptr_array_remove_index (data->resolvers, data->resolvers->len - 1);
+
+  if (data->resolvers->len > 0)
+    {
+      IdeSymbolResolver *resolver;
+      GCancellable *cancellable;
+
+      resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
+      cancellable = ide_task_get_cancellable (task);
+
+      ide_symbol_resolver_lookup_symbol_async (resolver,
+                                               data->location,
+                                               cancellable,
+                                               ide_buffer_get_symbol_at_location_cb,
+                                               g_steal_pointer (&task));
+    }
+  else if (data->symbol == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_FOUND,
+                                 "Symbol not found");
+    }
+  else
+    {
+      ide_task_return_pointer (task,
+                               g_steal_pointer (&data->symbol),
+                               g_object_unref);
+    }
+}
+
+/**
+ * ide_buffer_get_symbol_at_location_async:
+ * @self: an #IdeBuffer
+ * @location: a #GtkTextIter indicating a position to search for a symbol
+ * @cancellable: a #GCancellable
+ * @callback: a #GAsyncReadyCallback
+ * @user_data: a #gpointer to hold user data
+ *
+ * Asynchronously get a possible symbol at @location.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_get_symbol_at_location_async (IdeBuffer           *self,
+                                         const GtkTextIter   *location,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  g_autoptr(IdeLocation) srcloc = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) resolvers = NULL;
+  IdeSymbolResolver *resolver;
+  LookUpSymbolData *data;
+  guint line;
+  guint line_offset;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (location != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  resolvers = ide_buffer_get_symbol_resolvers (self);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (resolvers, g_object_unref);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_get_symbol_at_location_async);
+
+  if (resolvers->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 _("The current language lacks a symbol resolver."));
+      return;
+    }
+
+  _ide_buffer_sync_to_unsaved_files (self);
+
+  line = gtk_text_iter_get_line (location);
+  line_offset = gtk_text_iter_get_line_offset (location);
+  srcloc = ide_location_new (ide_buffer_get_file (self), line, line_offset);
+
+  data = g_slice_new0 (LookUpSymbolData);
+  data->resolvers = g_steal_pointer (&resolvers);
+  data->location = g_steal_pointer (&srcloc);
+  ide_task_set_task_data (task, data, lookup_symbol_data_free);
+
+  /* Try lookup_symbol on each symbol resolver one by by one. */
+  resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
+  ide_symbol_resolver_lookup_symbol_async (resolver,
+                                           data->location,
+                                           cancellable,
+                                           ide_buffer_get_symbol_at_location_cb,
+                                           g_steal_pointer (&task));
+}
+
+/**
+ * ide_buffer_get_symbol_at_location_finish:
+ * @self: an #IdeBuffer
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError
+ *
+ * Completes an asynchronous request to locate a symbol at a location.
+ *
+ * Returns: (transfer full): An #IdeSymbol or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeSymbol *
+ide_buffer_get_symbol_at_location_finish (IdeBuffer     *self,
+                                          GAsyncResult  *result,
+                                          GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+/**
+ * ide_buffer_get_selection_bounds:
+ * @self: an #IdeBuffer
+ * @insert: (out): a #GtkTextIter to get the insert position
+ * @selection: (out): a #GtkTextIter to get the selection position
+ *
+ * This function acts like gtk_text_buffer_get_selection_bounds() except that
+ * it always places the location of the insert mark at @insert and the location
+ * of the selection mark at @selection.
+ *
+ * Calling gtk_text_iter_order() with the results of this function would be
+ * equivalent to calling gtk_text_buffer_get_selection_bounds().
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_get_selection_bounds (IdeBuffer   *self,
+                                 GtkTextIter *insert,
+                                 GtkTextIter *selection)
+{
+  GtkTextMark *mark;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  if (insert != NULL)
+    {
+      mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (self));
+      gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (self), insert, mark);
+    }
+
+  if (selection != NULL)
+    {
+      mark = gtk_text_buffer_get_selection_bound (GTK_TEXT_BUFFER (self));
+      gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (self), selection, mark);
+    }
+}
+
+/**
+ * ide_buffer_trim_trailing_whitespace:
+ * @self: an #IdeBuffer
+ *
+ * Trim trailing whitespaces from the buffer.
+ *
+ * Only lines that are marked as changed by the underlying buffer
+ * monitor will be trimmed. If no #IdeBufferChangeMonitor is present,
+ * then all lines will be trimmed.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_trim_trailing_whitespace (IdeBuffer *self)
+{
+  GtkTextBuffer *buffer;
+  GtkTextIter iter;
+  gint line;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  buffer = GTK_TEXT_BUFFER (self);
+
+  gtk_text_buffer_get_end_iter (buffer, &iter);
+
+  for (line = gtk_text_iter_get_line (&iter); line >= 0; line--)
+    {
+      IdeBufferLineChange change = IDE_BUFFER_LINE_CHANGE_CHANGED;
+
+      if (self->change_monitor)
+        change = ide_buffer_change_monitor_get_change (self->change_monitor, line);
+
+      if (change != IDE_BUFFER_LINE_CHANGE_NONE)
+        {
+          gtk_text_buffer_get_iter_at_line (buffer, &iter, line);
+
+/*
+ * Preserve all whitespace that isn't space or tab.
+ * This could include line feed, form feed, etc.
+ */
+#define TEXT_ITER_IS_SPACE(ptr) \
+  ({  \
+    gunichar ch = gtk_text_iter_get_char (ptr); \
+    (ch == ' ' || ch == '\t'); \
+  })
+
+          /*
+           * Move to the first character at the end of the line (skipping the newline)
+           * and progress to trip if it is white space.
+           */
+          if (gtk_text_iter_forward_to_line_end (&iter) &&
+              !gtk_text_iter_starts_line (&iter) &&
+              gtk_text_iter_backward_char (&iter) &&
+              TEXT_ITER_IS_SPACE (&iter))
+            {
+              GtkTextIter begin = iter;
+
+              gtk_text_iter_forward_to_line_end (&iter);
+
+              while (TEXT_ITER_IS_SPACE (&begin))
+                {
+                  if (gtk_text_iter_starts_line (&begin))
+                    break;
+
+                  if (!gtk_text_iter_backward_char (&begin))
+                    break;
+                }
+
+              if (!TEXT_ITER_IS_SPACE (&begin) && !gtk_text_iter_ends_line (&begin))
+                gtk_text_iter_forward_char (&begin);
+
+              if (!gtk_text_iter_equal (&begin, &iter))
+                gtk_text_buffer_delete (buffer, &begin, &iter);
+            }
+
+#undef TEXT_ITER_IS_SPACE
+        }
+    }
+}
+
+static void
+ide_buffer_get_symbol_resolvers_cb (IdeExtensionSetAdapter *set,
+                                    PeasPluginInfo         *plugin_info,
+                                    PeasExtension          *exten,
+                                    gpointer                user_data)
+{
+  IdeSymbolResolver *resolver = (IdeSymbolResolver *)exten;
+  GPtrArray *ar = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
+  g_assert (ar != NULL);
+
+  g_ptr_array_add (ar, g_object_ref (resolver));
+}
+
+/**
+ * ide_buffer_get_symbol_resolvers:
+ * @self: an #IdeBuffer
+ *
+ * Gets the symbol resolvers for the buffer based on the current language. The
+ * resolvers in the resulting array are sorted by priority.
+ *
+ * Returns: (transfer full) (element-type IdeSymbolResolver): a #GPtrArray
+ *   of #IdeSymbolResolver.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_buffer_get_symbol_resolvers (IdeBuffer *self)
+{
+  GPtrArray *ar;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  ar = g_ptr_array_new_with_free_func (g_object_unref);
+
+  if (self->symbol_resolvers != NULL)
+    ide_extension_set_adapter_foreach_by_priority (self->symbol_resolvers,
+                                                   ide_buffer_get_symbol_resolvers_cb,
+                                                   ar);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ar);
+}
+
+/**
+ * ide_buffer_get_line_text:
+ * @self: a #IdeBuffer
+ * @line: a line number starting from 0
+ *
+ * Gets the contents of a single line within the buffer.
+ *
+ * Returns: (transfer full) (nullable): a string containing the line's text
+ *   or %NULL if the line does not exist.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_buffer_get_line_text (IdeBuffer *self,
+                          guint      line)
+{
+  GtkTextIter begin;
+
+  g_assert (IDE_IS_BUFFER (self));
+
+  gtk_text_buffer_get_iter_at_line (GTK_TEXT_BUFFER (self), &begin, line);
+
+  if (gtk_text_iter_get_line (&begin) == line)
+    {
+      GtkTextIter end = begin;
+
+      if (gtk_text_iter_ends_line (&end) ||
+          gtk_text_iter_forward_to_line_end (&end))
+        return gtk_text_iter_get_slice (&begin, &end);
+    }
+
+  return g_strdup ("");
+}
+
+static void
+ide_buffer_guess_language (IdeBuffer *self)
+{
+  GtkSourceLanguageManager *manager;
+  GtkSourceLanguage *lang;
+  g_autofree gchar *basename = NULL;
+  g_autofree gchar *content_type = NULL;
+  g_autofree gchar *line = NULL;
+  const gchar *path;
+  GFile *file;
+  gboolean uncertain = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  line = ide_buffer_get_line_text (self, 0);
+  file = ide_buffer_get_file (self);
+
+  if (!g_file_is_native (file))
+    path = basename = g_file_get_basename (file);
+  else
+    path = g_file_peek_path (file);
+
+  content_type = g_content_type_guess (path, (const guchar *)line, strlen (line), &uncertain);
+  if (uncertain)
+    return;
+
+  manager = gtk_source_language_manager_get_default ();
+  if (!(lang = gtk_source_language_manager_guess_language (manager, path, content_type)))
+    return;
+
+  if (!ide_str_equal0 (gtk_source_language_get_id (lang), ide_buffer_get_language_id (self)))
+    gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (self), lang);
+}
+
+gboolean
+_ide_buffer_can_restore_cursor (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->can_restore_cursor;
+}
+
+void
+_ide_buffer_cancel_cursor_restore (IdeBuffer *self)
+{
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  self->can_restore_cursor = FALSE;
+}
+
+/**
+ * ide_buffer_hold:
+ * @self: a #IdeBuffer
+ *
+ * Increases the "hold count" of the #IdeBuffer by one.
+ *
+ * The hold count is similar to a reference count, as it allows the buffer
+ * manager to know when a buffer may be destroyed cleanly.
+ *
+ * Doing so ensures that the buffer wont be unloaded or have reference
+ * cycles broken.
+ *
+ * Release the hold with ide_buffer_release().
+ *
+ * When the hold count reaches zero, the buffer will be destroyed.
+ *
+ * Returns: (transfer full): @self
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_buffer_hold (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  self->hold++;
+
+  return g_object_ref (self);
+}
+
+/**
+ * ide_buffer_release:
+ * @self: a #IdeBuffer
+ *
+ * Releases the "hold count" on a buffer.
+ *
+ * The buffer will be destroyed and unloaded when the hold count
+ * reaches zero.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_release (IdeBuffer *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (self->hold > 0);
+
+  self->hold--;
+
+  if (self->hold == 0)
+    {
+      IdeObjectBox *box = ide_object_box_from_object (G_OBJECT (self));
+
+      if (box != NULL)
+        ide_object_destroy (IDE_OBJECT (box));
+    }
+
+  g_object_unref (self);
+}
+
+IdeExtensionSetAdapter *
+_ide_buffer_get_addins (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->addins;
+}
+
+void
+_ide_buffer_line_flags_changed (IdeBuffer *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  g_signal_emit (self, signals [LINE_FLAGS_CHANGED], 0);
+}
+
+/**
+ * ide_buffer_has_symbol_resolvers:
+ * @self: a #IdeBuffer
+ *
+ * Checks if any symbol resolvers are available.
+ *
+ * Returns: %TRUE if at least one symbol resolvers is available
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_has_symbol_resolvers (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->symbol_resolvers != NULL &&
+         ide_extension_set_adapter_get_n_extensions (self->symbol_resolvers) > 0;
+}
diff --git a/src/libide/code/ide-buffer.h b/src/libide/code/ide-buffer.h
new file mode 100644
index 000000000..c6d5ac636
--- /dev/null
+++ b/src/libide/code/ide-buffer.h
@@ -0,0 +1,178 @@
+/* ide-buffer.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <gtksourceview/gtksource.h>
+#include <libide-core.h>
+
+#include "ide-buffer-change-monitor.h"
+#include "ide-diagnostics.h"
+#include "ide-file-settings.h"
+#include "ide-formatter.h"
+#include "ide-location.h"
+#include "ide-range.h"
+#include "ide-rename-provider.h"
+#include "ide-symbol.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUFFER (ide_buffer_get_type())
+
+typedef enum
+{
+  IDE_BUFFER_STATE_READY,
+  IDE_BUFFER_STATE_LOADING,
+  IDE_BUFFER_STATE_SAVING,
+  IDE_BUFFER_STATE_FAILED,
+} IdeBufferState;
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeBuffer, ide_buffer, IDE, BUFFER, GtkSourceBuffer)
+
+IDE_AVAILABLE_IN_3_32
+GBytes                 *ide_buffer_dup_content                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gchar                  *ide_buffer_dup_title                     (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_format_selection_async        (IdeBuffer               *self,
+                                                                  IdeFormatterOptions     *options,
+                                                                  GCancellable            *cancellable,
+                                                                  GAsyncReadyCallback      callback,
+                                                                  gpointer                 user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_format_selection_finish       (IdeBuffer               *self,
+                                                                  GAsyncResult            *result,
+                                                                  GError                 **error);
+IDE_AVAILABLE_IN_3_32
+guint                   ide_buffer_get_change_count              (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeBufferChangeMonitor *ide_buffer_get_change_monitor            (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_changed_on_volume         (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostics         *ide_buffer_get_diagnostics               (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeLocation            *ide_buffer_get_insert_location           (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_is_temporary              (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_failed                    (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+const GError           *ide_buffer_get_failure                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gchar                  *ide_buffer_dup_uri                       (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+GFile                  *ide_buffer_get_file                      (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeFileSettings        *ide_buffer_get_file_settings             (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeFormatter           *ide_buffer_get_formatter                 (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_highlight_diagnostics     (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_get_iter_at_location          (IdeBuffer               *self,
+                                                                  GtkTextIter             *iter,
+                                                                  IdeLocation             *location);
+IDE_AVAILABLE_IN_3_32
+IdeLocation            *ide_buffer_get_iter_location             (IdeBuffer               *self,
+                                                                  const GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+const gchar            *ide_buffer_get_language_id               (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_language_id               (IdeBuffer               *self,
+                                                                  const gchar             *language_id);
+IDE_AVAILABLE_IN_3_32
+gchar                  *ide_buffer_get_line_text                 (IdeBuffer               *self,
+                                                                  guint                    line);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_loading                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_read_only                 (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeRenameProvider      *ide_buffer_get_rename_provider           (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_get_selection_bounds          (IdeBuffer               *self,
+                                                                  GtkTextIter             *insert,
+                                                                  GtkTextIter             *selection);
+IDE_AVAILABLE_IN_3_32
+IdeRange               *ide_buffer_get_selection_range           (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeBufferState          ide_buffer_get_state                     (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+const gchar            *ide_buffer_get_style_scheme_name         (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_get_symbol_at_location_async  (IdeBuffer               *self,
+                                                                  const GtkTextIter       *location,
+                                                                  GCancellable            *cancellable,
+                                                                  GAsyncReadyCallback      callback,
+                                                                  gpointer                 user_data);
+IDE_AVAILABLE_IN_3_32
+IdeSymbol              *ide_buffer_get_symbol_at_location_finish (IdeBuffer               *self,
+                                                                  GAsyncResult            *result,
+                                                                  GError                 **error);
+IDE_AVAILABLE_IN_3_32
+GPtrArray              *ide_buffer_get_symbol_resolvers          (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gchar                  *ide_buffer_get_word_at_iter              (IdeBuffer               *self,
+                                                                  const GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_has_diagnostics               (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_has_symbol_resolvers          (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuffer              *ide_buffer_hold                          (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext             *ide_buffer_ref_context                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_rehighlight                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_release                       (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_save_file_async               (IdeBuffer               *self,
+                                                                  GFile                   *file,
+                                                                  GCancellable            *cancellable,
+                                                                  IdeNotification        **notif,
+                                                                  GAsyncReadyCallback      callback,
+                                                                  gpointer                 user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_save_file_finish              (IdeBuffer               *self,
+                                                                  GAsyncResult            *result,
+                                                                  GError                 **error);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_change_monitor            (IdeBuffer               *self,
+                                                                  IdeBufferChangeMonitor  *change_monitor);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_diagnostics               (IdeBuffer               *self,
+                                                                  IdeDiagnostics          *diagnostics);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_highlight_diagnostics     (IdeBuffer               *self,
+                                                                  gboolean                 
highlight_diagnostics);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_style_scheme_name         (IdeBuffer               *self,
+                                                                  const gchar             
*style_scheme_name);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_trim_trailing_whitespace      (IdeBuffer               *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-unsaved-file-private.h b/src/libide/code/ide-unsaved-file-private.h
new file mode 100644
index 000000000..99d8ba3e0
--- /dev/null
+++ b/src/libide/code/ide-unsaved-file-private.h
@@ -0,0 +1,32 @@
+/* ide-unsaved-file-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-unsaved-file.h"
+
+G_BEGIN_DECLS
+
+IdeUnsavedFile *_ide_unsaved_file_new (GFile       *file,
+                                       GBytes      *content,
+                                       const gchar *temp_path,
+                                       gint64       sequence);
+
+G_END_DECLS
diff --git a/src/libide/buffers/ide-unsaved-file.c b/src/libide/code/ide-unsaved-file.c
similarity index 94%
rename from src/libide/buffers/ide-unsaved-file.c
rename to src/libide/code/ide-unsaved-file.c
index 04b6abb99..c371e51b3 100644
--- a/src/libide/buffers/ide-unsaved-file.c
+++ b/src/libide/code/ide-unsaved-file.c
@@ -22,11 +22,10 @@
 
 #include "config.h"
 
-#include "ide-debug.h"
-
-#include "application/ide-application.h"
-#include "buffers/ide-unsaved-file.h"
-#include "buffers/ide-buffer-private.h"
+#include "ide-buffer.h"
+#include "ide-buffer-private.h"
+#include "ide-unsaved-file.h"
+#include "ide-unsaved-file-private.h"
 
 /*
  * This type is meant to be created and then immutable after that.
@@ -34,8 +33,7 @@
  * any other thread to do the work.
  */
 
-G_DEFINE_BOXED_TYPE (IdeUnsavedFile, ide_unsaved_file,
-                     ide_unsaved_file_ref, ide_unsaved_file_unref)
+G_DEFINE_BOXED_TYPE (IdeUnsavedFile, ide_unsaved_file, ide_unsaved_file_ref, ide_unsaved_file_unref)
 
 struct _IdeUnsavedFile
 {
diff --git a/src/libide/buffers/ide-unsaved-file.h b/src/libide/code/ide-unsaved-file.h
similarity index 90%
rename from src/libide/buffers/ide-unsaved-file.h
rename to src/libide/code/ide-unsaved-file.h
index 1223caa02..9e72bc65a 100644
--- a/src/libide/buffers/ide-unsaved-file.h
+++ b/src/libide/code/ide-unsaved-file.h
@@ -20,11 +20,13 @@
 
 #pragma once
 
-#include <gio/gio.h>
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
-#include "ide-types.h"
+#include "ide-code-types.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/buffers/ide-unsaved-files.c b/src/libide/code/ide-unsaved-files.c
similarity index 89%
rename from src/libide/buffers/ide-unsaved-files.c
rename to src/libide/code/ide-unsaved-files.c
index 00dafdae6..19b4ae9a9 100644
--- a/src/libide/buffers/ide-unsaved-files.c
+++ b/src/libide/code/ide-unsaved-files.c
@@ -29,18 +29,12 @@
 #include <glib/gstdio.h>
 #include <string.h>
 
-#include "ide-context.h"
-#include "ide-debug.h"
-#include "ide-global.h"
-
-#include "application/ide-application.h"
-#include "buffers/ide-buffer-private.h"
-#include "buffers/ide-unsaved-file.h"
-#include "buffers/ide-unsaved-files.h"
-#include "projects/ide-project.h"
-#include "threading/ide-task.h"
-#include "util/ide-glib.h"
-#include "util/ide-line-reader.h"
+#include <libide-io.h>
+#include <libide-threading.h>
+
+#include "ide-unsaved-file.h"
+#include "ide-unsaved-file-private.h"
+#include "ide-unsaved-files.h"
 
 typedef struct
 {
@@ -58,6 +52,7 @@ struct _IdeUnsavedFiles
   GMutex     mutex;
   GPtrArray *unsaved_files;
   gint64     sequence;
+  gchar     *project_id;
 };
 
 typedef struct
@@ -68,26 +63,28 @@ typedef struct
 
 G_DEFINE_TYPE (IdeUnsavedFiles, ide_unsaved_files, IDE_TYPE_OBJECT)
 
+enum {
+  PROP_0,
+  PROP_PROJECT_ID,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
 static void ide_unsaved_files_update_locked (IdeUnsavedFiles *self,
                                              GFile           *file,
                                              GBytes          *content);
 
 static gchar *
-get_drafts_directory (IdeContext *context)
+get_drafts_directory (IdeUnsavedFiles *self)
 {
-  IdeProject *project;
-  const gchar *project_name;
-
   g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_CONTEXT (context));
-
-  project = ide_context_get_project (context);
-  project_name = ide_project_get_id (project);
+  g_assert (IDE_IS_UNSAVED_FILES (self));
 
   return g_build_filename (g_get_user_data_dir (),
                            ide_get_program_name (),
                            "drafts",
-                           project_name,
+                           self->project_id,
                            NULL);
 }
 
@@ -269,17 +266,14 @@ ide_unsaved_files_save_worker (IdeTask      *task,
 static AsyncState *
 async_state_new (IdeUnsavedFiles *files)
 {
-  IdeContext *context;
   AsyncState *state;
 
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_UNSAVED_FILES (files));
 
-  context = ide_object_get_context (IDE_OBJECT (files));
-
   state = g_slice_new0 (AsyncState);
   state->unsaved_files = g_ptr_array_new_with_free_func (unsaved_file_free);
-  state->drafts_directory = get_drafts_directory (context);
+  state->drafts_directory = get_drafts_directory (files);
 
   return state;
 }
@@ -404,7 +398,7 @@ ide_unsaved_files_restore_worker (IdeTask      *task,
 
       line[line_len] = '\0';
 
-      if (dzl_str_empty0 (line))
+      if (ide_str_empty0 (line))
         continue;
 
       file = g_file_new_for_uri (line);
@@ -514,7 +508,6 @@ static void
 ide_unsaved_files_remove_draft_locked (IdeUnsavedFiles *self,
                                        GFile           *file)
 {
-  IdeContext *context;
   g_autofree gchar *drafts_directory = NULL;
   g_autofree gchar *uri = NULL;
   g_autofree gchar *hash = NULL;
@@ -526,8 +519,7 @@ ide_unsaved_files_remove_draft_locked (IdeUnsavedFiles *self,
   g_assert (IDE_IS_UNSAVED_FILES (self));
   g_assert (G_IS_FILE (file));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  drafts_directory = get_drafts_directory (context);
+  drafts_directory = get_drafts_directory (self);
   uri = g_file_get_uri (file);
   hash = hash_uri (uri);
   path = g_build_filename (drafts_directory, hash, NULL);
@@ -824,6 +816,45 @@ ide_unsaved_files_get_sequence (IdeUnsavedFiles *self)
   return ret;
 }
 
+static void
+ide_unsaved_files_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeUnsavedFiles *self = IDE_UNSAVED_FILES (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      g_value_set_string (value, self->project_id);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_unsaved_files_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeUnsavedFiles *self = IDE_UNSAVED_FILES (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      self->project_id = g_value_dup_string (value);
+      g_assert (self->project_id != NULL);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
 static void
 ide_unsaved_files_finalize (GObject *object)
 {
@@ -831,8 +862,9 @@ ide_unsaved_files_finalize (GObject *object)
 
   g_assert (IDE_IS_MAIN_THREAD ());
 
-  g_mutex_clear (&self->mutex);
   g_clear_pointer (&self->unsaved_files, g_ptr_array_unref);
+  g_clear_pointer (&self->project_id, g_free);
+  g_mutex_clear (&self->mutex);
 
   G_OBJECT_CLASS (ide_unsaved_files_parent_class)->finalize (object);
 }
@@ -843,6 +875,17 @@ ide_unsaved_files_class_init (IdeUnsavedFilesClass *klass)
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
   object_class->finalize = ide_unsaved_files_finalize;
+  object_class->get_property = ide_unsaved_files_get_property;
+  object_class->set_property = ide_unsaved_files_set_property;
+
+  properties [PROP_PROJECT_ID] =
+    g_param_spec_string ("project-id",
+                         "Project Id",
+                         "The identifier for the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
 static void
@@ -941,3 +984,39 @@ ide_unsaved_files_reap_finish (IdeUnsavedFiles  *self,
 
   return ide_task_propagate_boolean (IDE_TASK (result), error);
 }
+
+/**
+ * ide_unsaved_files_from_context:
+ * @context: an #IdeContext
+ *
+ * Gets the unsaved files object for @context.
+ *
+ * Returns: (transfer none): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeUnsavedFiles *
+ide_unsaved_files_from_context (IdeContext *context)
+{
+  IdeUnsavedFiles *self;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  ide_object_lock (IDE_OBJECT (context));
+  self = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_UNSAVED_FILES);
+  if (self == NULL)
+    {
+      g_autofree gchar *project_id = ide_context_dup_project_id (context);
+      self = g_object_new (IDE_TYPE_UNSAVED_FILES,
+                           "project-id", project_id,
+                           NULL);
+      ide_object_append (IDE_OBJECT (context), IDE_OBJECT (self));
+    }
+  ide_object_unlock (IDE_OBJECT (context));
+
+  /* Looks unsafe because we get a full ref back */
+  g_object_unref (self);
+
+  return self;
+}
diff --git a/src/libide/code/ide-unsaved-files.h b/src/libide/code/ide-unsaved-files.h
new file mode 100644
index 000000000..0759d7cb5
--- /dev/null
+++ b/src/libide/code/ide-unsaved-files.h
@@ -0,0 +1,87 @@
+/* ide-unsaved-files.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_UNSAVED_FILES (ide_unsaved_files_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeUnsavedFiles, ide_unsaved_files, IDE, UNSAVED_FILES, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeUnsavedFiles *ide_unsaved_files_from_context     (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_update           (IdeUnsavedFiles      *self,
+                                                     GFile                *file,
+                                                     GBytes               *content);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_remove           (IdeUnsavedFiles      *self,
+                                                     GFile                *file);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_save_async       (IdeUnsavedFiles      *files,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_unsaved_files_save_finish      (IdeUnsavedFiles      *files,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_restore_async    (IdeUnsavedFiles      *files,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_unsaved_files_restore_finish   (IdeUnsavedFiles      *files,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+IDE_AVAILABLE_IN_3_32
+GPtrArray       *ide_unsaved_files_to_array         (IdeUnsavedFiles      *self);
+IDE_AVAILABLE_IN_3_32
+gint64           ide_unsaved_files_get_sequence     (IdeUnsavedFiles      *files);
+IDE_AVAILABLE_IN_3_32
+IdeUnsavedFile  *ide_unsaved_files_get_unsaved_file (IdeUnsavedFiles      *self,
+                                                    GFile                *file);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_clear            (IdeUnsavedFiles      *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_unsaved_files_contains         (IdeUnsavedFiles      *self,
+                                                    GFile                *file);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_reap_async       (IdeUnsavedFiles      *self,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_unsaved_files_reap_finish      (IdeUnsavedFiles      *self,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+
+G_END_DECLS


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