/* 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-io.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-action-provider.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-gfile-private.h"
#include "ide-highlight-engine.h"
#include "ide-location.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_UNUSED           "diagnostician::unused"
#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 UNUSED_COLOR         "#c17d11"
#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;
  IdeExtensionAdapter    *code_action_provider;
  IdeBufferManager       *buffer_manager;
  IdeBufferChangeMonitor *change_monitor;
  GBytes                 *content;
  IdeDiagnostics         *diagnostics;
  GError                 *failure;
  IdeFileSettings        *file_settings;
  IdeHighlightEngine     *highlight_engine;
  GtkSourceFile          *source_file;
  GFile                  *readlink_file;

  IdeTask                *in_flight_symbol_at_location;
  gint                    in_flight_symbol_at_location_pos;

  /* Scalars */
  guint                   change_count;
  guint                   settling_source;
  gint                    hold;

  /* Bit-fields */
  IdeBufferState          state : 3;
  guint                   can_restore_cursor : 1;
  guint                   is_temporary : 1;
  guint                   enable_addins : 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;
  GtkSourceFile   *source_file;
} SaveState;

typedef struct
{
  GPtrArray   *resolvers;
  IdeLocation *location;
  IdeSymbol   *symbol;
} LookUpSymbolData;

G_DEFINE_FINAL_TYPE (IdeBuffer, ide_buffer, GTK_SOURCE_TYPE_BUFFER)

enum {
  PROP_0,
  PROP_BUFFER_MANAGER,
  PROP_CHANGE_MONITOR,
  PROP_CHANGED_ON_VOLUME,
  PROP_ENABLE_ADDINS,
  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,
  REQUEST_SCROLL_TO_INSERT,
  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     settle_async                       (IdeBuffer              *self,
                                                    GCancellable           *cancellable,
                                                    GAsyncReadyCallback     callback,
                                                    gpointer                user_data);
static gboolean settle_finish                      (IdeBuffer              *self,
                                                    GAsyncResult           *result,
                                                    GError                **error);

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_clear_object (&state->source_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          enable_addins,
                 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,
                       "enable-addins", enable_addins,
                       "is-temporary", is_temporary,
                       "implicit-trailing-newline", FALSE,
                       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);
      g_clear_object (&self->readlink_file);
      self->readlink_file = _ide_g_file_readlink (file);
      ide_buffer_reload_file_settings (self);

      if (self->addins != NULL && self->enable_addins)
        {
          IdeBufferFileLoad closure = { self, file };
          ide_extension_set_adapter_foreach (self->addins,
                                             _ide_buffer_addin_file_loaded_cb,
                                             &closure);
        }

      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE]);
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
    }
}

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_update_implicit_newline (IdeBuffer *self)
{
  IdeFileSettings *file_settings;
  gboolean was_implicit;
  gboolean now_implicit;

  IDE_ENTRY;

  g_assert (IDE_IS_BUFFER (self));
  g_assert (!ide_buffer_get_loading (self));

  if (!(file_settings = ide_buffer_get_file_settings (self)))
    IDE_EXIT;

  /* If the new file-settings object does not match our
   * current setting for trailing newlines, then we need
   * to adjust that immediately to ensure our buffer contains
   * the contents as expected.
   *
   * Otherwise, buffer change monitors will not line up with
   * the the expectation on storage.
   */
  was_implicit = gtk_source_buffer_get_implicit_trailing_newline (GTK_SOURCE_BUFFER (self));
  now_implicit = ide_file_settings_get_insert_trailing_newline (file_settings);

  if (was_implicit != now_implicit)
    {
      GtkTextIter iter;

      gtk_source_buffer_set_implicit_trailing_newline (GTK_SOURCE_BUFFER (self), now_implicit);

      if (now_implicit)
        {
          gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (self), &iter);

          if (gtk_text_iter_starts_line (&iter))
            {
              GtkTextIter begin = iter;

              gtk_text_iter_backward_char (&begin);

              if (!gtk_text_iter_equal (&begin, &iter))
                {
                  gboolean modified = gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (self));

                  gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (self));
                  gtk_text_buffer_delete (GTK_TEXT_BUFFER (self), &begin, &iter);
                  gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (self));

                  if (!modified)
                    gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (self), modified);
                }
            }
        }
      else
        {
          gboolean modified = gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (self));

          gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (self), &iter);

          gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (self));
          gtk_text_buffer_insert (GTK_TEXT_BUFFER (self), &iter, "\n", 1);
          gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (self));

          if (!modified)
            gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (self), modified);
        }
    }

  IDE_EXIT;
}

static void
ide_buffer_real_loaded (IdeBuffer *self)
{
  g_assert (IDE_IS_BUFFER (self));

  ide_buffer_update_implicit_newline (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 && self->enable_addins)
    {
      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);

  if (self->code_action_provider)
    ide_extension_adapter_set_value (self->code_action_provider, 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->code_action_provider);
  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_object (&self->readlink_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_ENABLE_ADDINS:
      g_value_set_boolean (value, self->enable_addins);
      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_ENABLE_ADDINS:
      self->enable_addins = g_value_get_boolean (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:enable-addins:
   *
   * The "enable-addins" property determines whether addins will be aware of
   * this buffer. When set to %FALSE no ide_buffer_addin_*() functions will be
   * called on this buffer.
   *
   * Since: 41.0
   */
  properties [PROP_ENABLE_ADDINS] =
    g_param_spec_boolean ("enable-addins",
                          "Enable Addins",
                          "Whether to enable addins for this buffer",
                          TRUE,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));

  /**
   * 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);

  /**
   * IdeBuffer::request-scroll-to-insert:
   *
   * Requests that attached views scroll to insert location.
   *
   * This is generally only used when loading a buffer.
   *
   * Since: 3.32
   */
  signals [REQUEST_SCROLL_TO_INSERT] =
    g_signal_new_class_handler ("request-scroll-to-insert",
                                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 [REQUEST_SCROLL_TO_INSERT],
                              G_TYPE_FROM_CLASS (klass),
                              g_cclosure_marshal_VOID__VOIDv);
}

static void
ide_buffer_init (IdeBuffer *self)
{
  self->in_flight_symbol_at_location_pos = -1;
  self->source_file = gtk_source_file_new ();
  self->can_restore_cursor = TRUE;
  self->highlight_diagnostics = TRUE;
  self->enable_addins = 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_code_action_provider_notify_extension (IdeBuffer           *self,
                                                  GParamSpec          *pspec,
                                                  IdeExtensionAdapter *adapter)
{
  IdeCodeActionProvider *code_action_provider;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_BUFFER (self));
  g_assert (IDE_IS_EXTENSION_ADAPTER (adapter));

  if ((code_action_provider = ide_extension_adapter_get_extension (adapter)))
    ide_code_action_provider_load (code_action_provider);
}

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 our code action provider, if any */
  self->code_action_provider = ide_extension_adapter_new (parent,
                                                          peas_engine_get_default (),
                                                          IDE_TYPE_CODE_ACTION_PROVIDER,
                                                          "Code-Action-Languages",
                                                          ide_buffer_get_language_id (self));

  g_signal_connect_object (self->code_action_provider,
                           "notify::extension",
                           G_CALLBACK (ide_buffer_code_action_provider_notify_extension),
                           self,
                           G_CONNECT_SWAPPED);
  ide_buffer_code_action_provider_notify_extension (self, NULL, self->code_action_provider);

  /* 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);

  /* Assume we are at newest state at end of file-load operation */
  _ide_buffer_set_changed_on_volume (self, FALSE);

  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,
                             IdeNotification      *notif,
                             GCancellable         *cancellable,
                             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);

  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 = notif ? g_object_ref (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);

  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);

  /* Guess the syntax language now if necessary */
  if (!gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self)))
    ide_buffer_guess_language (self);

  /* 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 && self->enable_addins)
    {
      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);

  /* Treat our save as freshest. It's possible we race, as we'd need an etag to
   * detect that, probably fine in all but the most slowest of races.
   */
  _ide_buffer_set_changed_on_volume (self, FALSE);

  /* Notify addins that a save has completed */
  if (self->addins != NULL && self->enable_addins)
    {
      IdeBufferFileSave closure = { self, state->file };
      ide_extension_set_adapter_foreach (self->addins,
                                         _ide_buffer_addin_file_saved_cb,
                                         &closure);
    }

  if (self->buffer_manager != NULL)
    _ide_buffer_manager_buffer_saved (self->buffer_manager, self);
  else
    g_critical ("Attempt to save buffer without access to buffer-manager");

  ide_task_return_boolean (task, TRUE);

  IDE_EXIT;
}

static void
ide_buffer_save_file_settle_cb (GObject      *object,
                                GAsyncResult *result,
                                gpointer      user_data)
{
  IdeBuffer *self = (IdeBuffer *)object;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GtkSourceFileSaver) saver = NULL;
  SaveState *state;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_BUFFER (self));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  settle_finish (self, result, NULL);

  state = ide_task_get_task_data (task);

  g_assert (state != NULL);
  g_assert (G_IS_FILE (state->file));
  g_assert (IDE_IS_NOTIFICATION (state->notif));
  g_assert (GTK_SOURCE_IS_FILE (state->source_file));

  if (self->addins != NULL && self->enable_addins)
    {
      IdeBufferFileSave closure = { self, state->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), state->source_file);
  /* At this point, we've notified the user of changes to the underlying file using
   * the infobar, so just save the file knowing that we are overwriting things.
   */
  gtk_source_file_saver_set_flags (saver,
                                   (GTK_SOURCE_FILE_SAVER_FLAGS_IGNORE_INVALID_CHARS |
                                    GTK_SOURCE_FILE_SAVER_FLAGS_IGNORE_MODIFICATION_TIME));
  gtk_source_file_saver_save_async (saver,
                                    G_PRIORITY_DEFAULT,
                                    ide_task_get_cancellable (task),
                                    ide_buffer_progress_cb,
                                    g_object_ref (state->notif),
                                    g_object_unref,
                                    ide_buffer_save_file_cb,
                                    g_object_ref (task));

}

/**
 * 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(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;

      /* The buffer might be empty, so mark it as modified so we really save */
      gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (self), TRUE);

      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);

  /* Keep buffer alive during save operation */
  ide_buffer_hold (self);
  g_signal_connect_object (task,
                           "notify::completed",
                           G_CALLBACK (ide_buffer_release),
                           self,
                           G_CONNECT_SWAPPED);

  if (self->state == IDE_BUFFER_STATE_SAVING)
    {
      /* TODO: We could save in-flight tasks and chain to them */
      ide_task_return_boolean (task, TRUE);
      IDE_EXIT;
    }

  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;
    }

  state->source_file = g_object_ref (source_file);

  /* Possibly avoid any writing if we can detect a no-change state */
  if (file == NULL || g_file_equal (file, ide_buffer_get_file (self)))
    {
      if (!self->changed_on_volume &&
          !gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (self)))
        {
          ide_notification_set_progress (local_notif, 1.0);
          ide_task_return_boolean (task, TRUE);
          IDE_GOTO (set_out_param);
        }
    }

  ide_buffer_set_state (self, IDE_BUFFER_STATE_SAVING);

  settle_async (self,
                cancellable,
                ide_buffer_save_file_settle_cb,
                g_steal_pointer (&task));

set_out_param:
  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));
  g_assert (IDE_IS_FILE_SETTINGS (file_settings));

  if (self->file_settings == file_settings)
    return;

  ide_clear_and_destroy_object (&self->file_settings);
  self->file_settings = g_object_ref (file_settings);

  if (!ide_buffer_get_loading (self))
    ide_buffer_update_implicit_newline (self);

  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);

  g_clear_object (&self->in_flight_symbol_at_location);
  self->in_flight_symbol_at_location_pos = -1;

  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 G_UNLIKELY (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;
      if (changed_on_volume)
        gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (self), TRUE);
      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_dup_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 && self->enable_addins)
    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)
    {
      IdeCodeActionProvider *code_action_provider;
      self->diagnostics = g_object_ref (diagnostics);
      code_action_provider = ide_extension_adapter_get_extension (self->code_action_provider);
      if (code_action_provider)
        ide_code_action_provider_set_diagnostics (IDE_CODE_ACTION_PROVIDER (code_action_provider), self->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_UNUSED)))
    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_UNUSED:
      tag_name = TAG_UNUSED;
      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;
    }

  n_ranges = ide_diagnostic_get_n_ranges (diagnostic);
  if (n_ranges == 0)
    {
      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_backward_char (&begin_iter);
            }
          else
            {
              /* Only highlight to next word */
              if (_ide_source_iter_inside_word (&end_iter) ||
                  _ide_source_iter_starts_word (&end_iter))
                _ide_source_iter_forward_visible_word_end (&end_iter);
              else
                gtk_text_iter_forward_to_line_end (&end_iter);
            }

          gtk_text_buffer_apply_tag_by_name (GTK_TEXT_BUFFER (self), tag_name, &begin_iter, &end_iter);
        }
    }

  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);

      /* Only persist if we have access to the object tree */
      if (self->buffer_manager != NULL &&
          !ide_object_in_destruction (IDE_OBJECT (self->buffer_manager)))
        {
          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);
}

static void
ide_buffer_query_code_action_cb(GObject      *object,
                                GAsyncResult *result,
                                gpointer user_data)
{
  IdeCodeActionProvider *code_action_provider = (IdeCodeActionProvider *)object;

  g_autoptr(GError) error = NULL;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GPtrArray) code_actions = NULL;

  g_assert(IDE_IS_CODE_ACTION_PROVIDER(object));
  g_assert(G_IS_ASYNC_RESULT(result));
  g_assert(IDE_IS_TASK(task));

  code_actions = ide_code_action_provider_query_finish(code_action_provider, result, &error);

  if (!code_actions)
    ide_task_return_error(task, g_steal_pointer(&error));
  else
    ide_task_return_pointer(task, g_steal_pointer(&code_actions), g_ptr_array_unref);
}

/**
 * ide_buffer_code_action_query_async:
 * @self: an #IdeBuffer
 * @cancellable: (nullable): a #GCancellable, or %NULL
 * @callback: the callback upon completion
 * @user_data: user data for @callback
 *
 * Queries for code actions in the current buffer.
 *
 * Since: 42.0
 */
void
ide_buffer_code_action_query_async(IdeBuffer           *self,
                                   GCancellable        *cancellable,
                                   GAsyncReadyCallback callback,
                                   gpointer user_data)
{
  g_autoptr(IdeTask)     task = NULL;
  IdeCodeActionProvider *code_action_provider;

  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));

  task = ide_task_new(self, cancellable, callback, user_data);
  ide_task_set_source_tag(task, ide_buffer_code_action_query_async);

  if (!(code_action_provider = ide_extension_adapter_get_extension(self->code_action_provider)))
    {
      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 code action provider registered for language %s",
                                language_id);

      IDE_EXIT;
    }

  ide_code_action_provider_query_async(code_action_provider,
                                       self,
                                       cancellable,
                                       ide_buffer_query_code_action_cb,
                                       g_steal_pointer(&task));

  IDE_EXIT;
}

/**
 * ide_buffer_code_action_query_finish:
 * @self: an #IdeBuffer
 * @result: a #GAsyncResult
 * @error: a location for a #GError, or %NULL
 *
 * Completes an asynchronous request to ide_buffer_query_code_action_async().
 *
 * Returns: (transfer full) (element-type IdeCodeAction): a #GPtrArray of #IdeCodeAction.
 *
 * Since: 42.0
 */
GPtrArray*
ide_buffer_code_action_query_finish(IdeBuffer     *self,
                                    GAsyncResult  *result,
                                    GError       **error)
{
  GPtrArray* ret;

  IDE_ENTRY;

  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);

  ret = ide_task_propagate_pointer(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 unused_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 (&unused_rgba, UNUSED_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_UNUSED,
                                                GET_TAG (TAG_UNUSED)))
        apply_style (GET_TAG (TAG_UNUSED),
                     "underline", PANGO_UNDERLINE_ERROR,
                     "underline-rgba", &unused_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 && self->enable_addins)
    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, TAG_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) unused_tag = NULL;
  g_autoptr(GtkTextTag) error_tag = NULL;
  g_autoptr(GtkTextTag) note_tag = NULL;
  g_autoptr(GtkTextTag) warning_tag = NULL;
  GdkRGBA deprecated_rgba;
  GdkRGBA unused_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 (&unused_rgba, UNUSED_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);
  unused_tag = gtk_text_tag_new (TAG_UNUSED);
  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_UNUSED, unused_tag))
    apply_style (unused_tag,
                 "underline", PANGO_UNDERLINE_ERROR,
                 "underline-rgba", &unused_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, unused_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;
  IdeBuffer *self;

  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);
  self = ide_task_get_source_object (task);
  g_assert (data->resolvers != NULL);
  g_assert (data->resolvers->len > 0);

  g_clear_object (&self->in_flight_symbol_at_location);
  self->in_flight_symbol_at_location_pos = -1;

  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_object (task, g_steal_pointer (&data->symbol));
    }
}

/**
 * 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;
    }

  /* If this query is the same as one in-flight, then try to chain
   * to that query instead of competing and duplicating work.
   */
  if (self->in_flight_symbol_at_location_pos == (int)gtk_text_iter_get_offset (location) &&
      self->in_flight_symbol_at_location != NULL)
    {
      ide_task_chain (self->in_flight_symbol_at_location, task);
      return;
    }
  else
    {
      g_set_object (&self->in_flight_symbol_at_location, task);
      self->in_flight_symbol_at_location_pos = gtk_text_iter_get_offset (location);
    }

  _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_object (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);
    }
}

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 *lang_id;
  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);

  basename = g_file_get_basename (file);

  if (!g_file_is_native (file))
    path = basename;
  else
    path = g_file_peek_path (file);

  manager = gtk_source_language_manager_get_default ();
  lang = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self));

  content_type = g_content_type_guess (path, (const guchar *)line, strlen (line), &uncertain);
  if (uncertain && lang != NULL)
    return;

  /* First try with full path, then with shortname */
  if (!(lang = gtk_source_language_manager_guess_language (manager, path, content_type)) &&
      !(lang = gtk_source_language_manager_guess_language (manager, basename, content_type)))
    return;

  lang_id = gtk_source_language_get_id (lang);

  /* Override to python3 by default for now, until shared-mime-info
   * gets a better way to detect the difference between the two.
   */
  if (ide_str_equal0 (lang_id, "python"))
    {
      lang_id = "python3";
      lang = gtk_source_language_manager_get_language (manager, lang_id);
    }

  if (!ide_str_equal0 (lang_id, 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;
}

static void
settle_cb (GObject      *object,
           GAsyncResult *result,
           gpointer      user_data)
{
  IdeBufferAddin *addin = (IdeBufferAddin *)object;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  gint *n_active;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_BUFFER_ADDIN (addin));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  n_active = ide_task_get_task_data (task);

  if (!ide_buffer_addin_settle_finish (addin, result, &error))
    g_warning ("Buffer addin \"%s\" failed to settle: %s",
               G_OBJECT_TYPE_NAME (addin),
               error->message);

  (*n_active)--;

  if (*n_active == 0)
    ide_task_return_boolean (task, TRUE);
}

static void
settle_foreach_cb (IdeExtensionSetAdapter *set,
                   PeasPluginInfo         *plugin_info,
                   PeasExtension          *exten,
                   gpointer                user_data)
{
  IdeBufferAddin *addin = (IdeBufferAddin *)exten;
  IdeTask *task = user_data;
  gint *n_active;

  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_BUFFER_ADDIN (addin));
  g_assert (IDE_IS_TASK (task));

  n_active = ide_task_get_task_data (task);

  (*n_active)++;

  ide_buffer_addin_settle_async (addin,
                                 ide_task_get_cancellable (task),
                                 settle_cb,
                                 g_object_ref (task));
}

static void
settle_async (IdeBuffer           *self,
              GCancellable        *cancellable,
              GAsyncReadyCallback  callback,
              gpointer             user_data)
{
  g_autoptr(IdeTask) task = NULL;
  gint *n_active;

  IDE_ENTRY;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_BUFFER (self));
  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));

  n_active = g_new0 (gint, 1);

  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, settle_async);
  ide_task_set_task_data (task, n_active, g_free);

  if (self->addins != NULL && self->enable_addins)
    ide_extension_set_adapter_foreach (self->addins,
                                       settle_foreach_cb,
                                       task);

  if (*n_active == 0)
    ide_task_return_boolean (task, TRUE);

  IDE_EXIT;
}

static gboolean
settle_finish (IdeBuffer     *self,
               GAsyncResult  *result,
               GError       **error)
{
  gboolean ret;

  IDE_ENTRY;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_BUFFER (self));
  g_assert (IDE_IS_TASK (result));

  ret = ide_task_propagate_boolean (IDE_TASK (result), error);

  IDE_RETURN (ret);
}

void
_ide_buffer_request_scroll_to_cursor (IdeBuffer *self)
{
  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_BUFFER (self));

  g_signal_emit (self, signals [REQUEST_SCROLL_TO_INSERT], 0);
}

gboolean
_ide_buffer_is_file (IdeBuffer *self,
                     GFile     *nolink_file)
{
  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
  g_return_val_if_fail (G_IS_FILE (nolink_file), FALSE);

  return g_file_equal (nolink_file, ide_buffer_get_file (self)) ||
         g_file_equal (nolink_file, self->readlink_file);
}
