IdeBufferChangeMonitor *
gbp_git_buffer_change_monitor_new (IdeBuffer         *buffer,
                                   IpcGitRepository  *repository,
                                   GFile             *file,
                                   GCancellable      *cancellable,
                                   GError           **error)
{
  GbpGitBufferChangeMonitor *ret;
  g_autoptr(IpcGitChangeMonitor) proxy = NULL;
  g_autoptr(IdeContext) context = NULL;
  g_autoptr(GFile) workdir = NULL;
  g_autofree gchar *relative_path = NULL;
  g_autofree gchar *obj_path = NULL;
  GDBusConnection *connection;

  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
  g_return_val_if_fail (G_IS_FILE (file), NULL);
  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);

  context = ide_buffer_ref_context (buffer);
  workdir = ide_context_ref_workdir (context);

  if (!g_file_has_prefix (file, workdir))
    {
      g_set_error (error,
                   G_IO_ERROR,
                   G_IO_ERROR_NOT_SUPPORTED,
                   "Cannot monitor files outside the working directory");
      return NULL;
    }

  relative_path = g_file_get_relative_path (workdir, file);

  if (!ipc_git_repository_call_create_change_monitor_sync (repository,
                                                           relative_path,
                                                           &obj_path,
                                                           cancellable,
                                                           error))
    return NULL;

  connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (repository));

  if (!(proxy = ipc_git_change_monitor_proxy_new_sync (connection,
                                                       G_DBUS_PROXY_FLAGS_NONE,
                                                       NULL,
                                                       obj_path,
                                                       cancellable,
                                                       error)))
    return NULL;

  ret = g_object_new (GBP_TYPE_GIT_BUFFER_CHANGE_MONITOR,
                      "buffer", buffer,
                      NULL);
  ret->proxy = g_steal_pointer (&proxy);

  return IDE_BUFFER_CHANGE_MONITOR (g_steal_pointer (&ret));
}
static void
ide_buffer_change_monitor_set_property (GObject      *object,
                                        guint         prop_id,
                                        const GValue *value,
                                        GParamSpec   *pspec)
{
  IdeBufferChangeMonitor *self = IDE_BUFFER_CHANGE_MONITOR (object);

  switch (prop_id)
    {
    case PROP_BUFFER:
      ide_buffer_change_monitor_set_buffer (self, g_value_get_object (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}
void
gbp_git_buffer_change_monitor_wait_async (GbpGitBufferChangeMonitor *self,
                                          GCancellable              *cancellable,
                                          GAsyncReadyCallback        callback,
                                          gpointer                   user_data)
{
  g_autoptr(IdeTask) task = NULL;
  IdeBuffer *buffer;
  guint change_count;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (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, gbp_git_buffer_change_monitor_wait_async);

  if (ide_task_return_error_if_cancelled (task))
    return;

  buffer = ide_buffer_change_monitor_get_buffer (IDE_BUFFER_CHANGE_MONITOR (self));
  change_count = ide_buffer_get_change_count (buffer);

  /* Update the peer of buffer contents immediately in-case it does
   * not yet have teh newest version.
   */
  if (change_count != self->last_change_count)
    {
      g_autoptr(GBytes) bytes = ide_buffer_dup_content (buffer);

      self->last_change_count = change_count;
      ipc_git_change_monitor_call_update_content (self->proxy,
                                                  (const gchar *)g_bytes_get_data (bytes, NULL),
                                                  NULL, NULL, NULL);
    }

  ipc_git_change_monitor_call_list_changes (self->proxy,
                                            cancellable,
                                            gbp_git_buffer_change_monitor_wait_cb,
                                            g_steal_pointer (&task));
}
static void
ide_git_buffer_change_monitor__buffer_delete_range_cb (IdeGitBufferChangeMonitor *self,
                                                       GtkTextIter               *begin,
                                                       GtkTextIter               *end,
                                                       IdeBuffer                 *buffer)
{
  IdeBufferLineChange change;

  g_assert (IDE_IS_GIT_BUFFER_CHANGE_MONITOR (self));
  g_assert (begin);
  g_assert (end);
  g_assert (IDE_IS_BUFFER (buffer));

  /*
   * We need to recalculate the diff when text is deleted if:
   *
   * 1) The range includes a newline.
   * 2) The current line change is set to NONE.
   *
   * Technically we need to do it on every change to be more correct, but that wastes a lot of
   * power. So instead, we'll be a bit lazy about it here and pick up the other changes on a much
   * more conservative timeout, generated by ide_git_buffer_change_monitor__buffer_changed_cb().
   */

  if (gtk_text_iter_get_line (begin) != gtk_text_iter_get_line (end))
    goto recalculate;

  change = ide_git_buffer_change_monitor_get_change (IDE_BUFFER_CHANGE_MONITOR (self), begin);
  if (change == IDE_BUFFER_LINE_CHANGE_NONE)
    goto recalculate;

  return;

recalculate:
  /*
   * We need to wait for the delete to occur, so mark it as necessary and let
   * ide_git_buffer_change_monitor__buffer_delete_range_after_cb perform the operation.
   */
  self->delete_range_requires_recalculation = TRUE;
}
static void
gbp_git_buffer_change_monitor_wait_cb (GObject      *object,
                                       GAsyncResult *result,
                                       gpointer      user_data)
{
  IpcGitChangeMonitor *proxy = (IpcGitChangeMonitor *)object;
  g_autoptr(GVariant) changes = NULL;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  GbpGitBufferChangeMonitor *self;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IPC_IS_GIT_CHANGE_MONITOR (proxy));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  self = ide_task_get_source_object (task);

  if (!ipc_git_change_monitor_call_list_changes_finish (proxy, &changes, result, &error))
    {
      g_clear_pointer (&self->cache, line_cache_free);
      self->not_found = TRUE;

      if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_FILE_NOT_FOUND))
        ide_task_return_boolean (task, TRUE);
      else
        ide_task_return_error (task, g_steal_pointer (&error));
    }
  else
    {
      g_clear_pointer (&self->cache, line_cache_free);
      self->not_found = FALSE;
      self->cache = line_cache_new_from_variant (changes);
      ide_buffer_change_monitor_emit_changed (IDE_BUFFER_CHANGE_MONITOR (self));
      ide_task_return_boolean (task, TRUE);
    }
}
static void
ide_git_buffer_change_monitor__buffer_insert_text_after_cb (IdeGitBufferChangeMonitor *self,
                                                            GtkTextIter               *location,
                                                            gchar                     *text,
                                                            gint                       len,
                                                            IdeBuffer                 *buffer)
{
  IdeBufferLineChange change;

  g_assert (IDE_IS_GIT_BUFFER_CHANGE_MONITOR (self));
  g_assert (location);
  g_assert (text);
  g_assert (IDE_IS_BUFFER (buffer));

  /*
   * We need to recalculate the diff when text is inserted if:
   *
   * 1) A newline is included in the text.
   * 2) The line currently has flags of NONE.
   *
   * Technically we need to do it on every change to be more correct, but that wastes a lot of
   * power. So instead, we'll be a bit lazy about it here and pick up the other changes on a much
   * more conservative timeout, generated by ide_git_buffer_change_monitor__buffer_changed_cb().
   */

  if (strchr (text, '\n') != NULL)
    goto recalculate;

  change = ide_git_buffer_change_monitor_get_change (IDE_BUFFER_CHANGE_MONITOR (self), location);
  if (change == IDE_BUFFER_LINE_CHANGE_NONE)
    goto recalculate;

  return;

recalculate:
  ide_git_buffer_change_monitor_recalculate (self);
}
static void
ide_git_buffer_change_monitor__calculate_cb (GObject      *object,
                                             GAsyncResult *result,
                                             gpointer      user_data_unused)
{
  IdeGitBufferChangeMonitor *self = (IdeGitBufferChangeMonitor *)object;
  g_autoptr(GHashTable) ret = NULL;
  g_autoptr(GError) error = NULL;

  g_assert (IDE_IS_GIT_BUFFER_CHANGE_MONITOR (self));

  self->in_calculation = FALSE;

  ret = ide_git_buffer_change_monitor_calculate_finish (self, result, &error);

  if (!ret)
    {
      if (!g_error_matches (error, GGIT_ERROR, GGIT_ERROR_NOTFOUND))
        g_message ("%s", error->message);
    }
  else
    {
      g_clear_pointer (&self->state, g_hash_table_unref);
      self->state = g_hash_table_ref (ret);
    }

  ide_buffer_change_monitor_emit_changed (IDE_BUFFER_CHANGE_MONITOR (self));

  /*
   * Recalculate the state if the buffer has changed since we submitted our request.
   */
  if (self->state_dirty)
    ide_git_buffer_change_monitor_calculate_async (self,
                                                   NULL,
                                                   ide_git_buffer_change_monitor__calculate_cb,
                                                   NULL);
}