Blob Blame History Raw
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- *
 * gtksourcecompletioncontext.c
 * This file is part of GtkSourceView
 *
 * Copyright (C) 2009 - Jesse van den Kieboom <jessevdk@gnome.org>
 *
 * GtkSourceView is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * GtkSourceView is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

/**
 * SECTION:completioncontext
 * @title: GtkSourceCompletionContext
 * @short_description: The context of a completion
 *
 * Initially, the completion window is hidden. For a completion to occur, it has
 * to be activated. The different possible activations are listed in
 * #GtkSourceCompletionActivation. When an activation occurs, a
 * #GtkSourceCompletionContext object is created, and the eligible providers are
 * asked to add proposals with gtk_source_completion_context_add_proposals().
 *
 * If no proposals are added, the completion window remains hidden, and the
 * context is destroyed.
 *
 * On the other hand, if proposals are added, the completion window becomes
 * visible, and the user can choose a proposal. If the user is not happy with
 * the shown proposals, he or she can insert or delete characters, to modify the
 * completion context and therefore hoping to see the proposal he or she wants.
 * This means that when an insertion or deletion occurs in the #GtkTextBuffer
 * when the completion window is visible, the eligible providers are again asked
 * to add proposals. The #GtkSourceCompletionContext:activation remains the
 * same in this case.
 *
 * When the completion window is hidden, the interactive completion is triggered
 * only on insertion in the buffer, not on deletion. Once the completion window
 * is visible, then on each insertion or deletion, there is a new population and
 * the providers are asked to add proposals. If there are no more proposals, the
 * completion window disappears. So if you want to keep the completion window
 * visible, but there are no proposals, you can insert a dummy proposal named
 * "No proposals". For example, the user types progressively the name of
 * a function, and some proposals appear. The user types a bad character and
 * there are no proposals anymore. What the user wants is to delete the last
 * character, and see the previous proposals. If the completion window
 * disappears, the previous proposals will not reappear on the character
 * deletion.
 *
 * A #GtkTextIter is associated with the context, this is where the completion
 * takes place. With this #GtkTextIter, you can get the associated
 * #GtkTextBuffer with gtk_text_iter_get_buffer().
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "gtksourcecompletioncontext.h"
#include "gtksourceview-enumtypes.h"
#include "gtksourcecompletionprovider.h"
#include "gtksourceview-i18n.h"
#include "gtksourcecompletion.h"

struct _GtkSourceCompletionContextPrivate
{
	GtkSourceCompletion *completion;

	GtkTextMark *mark;
	GtkSourceCompletionActivation activation;
};

enum
{
	PROP_0,
	PROP_COMPLETION,
	PROP_ITER,
	PROP_ACTIVATION
};

enum
{
	CANCELLED,
	N_SIGNALS
};

static guint context_signals[N_SIGNALS];

G_DEFINE_TYPE_WITH_PRIVATE (GtkSourceCompletionContext, gtk_source_completion_context, G_TYPE_INITIALLY_UNOWNED)

static void
gtk_source_completion_context_dispose (GObject *object)
{
	GtkSourceCompletionContext *context = GTK_SOURCE_COMPLETION_CONTEXT (object);

	if (context->priv->mark != NULL)
	{
		GtkTextBuffer *buffer = gtk_text_mark_get_buffer (context->priv->mark);

		if (buffer != NULL)
		{
			gtk_text_buffer_delete_mark (buffer, context->priv->mark);
		}

		g_object_unref (context->priv->mark);
		context->priv->mark = NULL;
	}

	g_clear_object (&context->priv->completion);

	G_OBJECT_CLASS (gtk_source_completion_context_parent_class)->dispose (object);
}

static void
set_iter (GtkSourceCompletionContext *context,
	  GtkTextIter                *iter)
{
	GtkTextBuffer *buffer;

	buffer = gtk_text_iter_get_buffer (iter);

	if (context->priv->mark != NULL)
	{
		GtkTextBuffer *old_buffer;

		old_buffer = gtk_text_mark_get_buffer (context->priv->mark);

		if (old_buffer != buffer)
		{
			g_object_unref (context->priv->mark);
			context->priv->mark = NULL;
		}
	}

	if (context->priv->mark == NULL)
	{
		context->priv->mark = gtk_text_buffer_create_mark (buffer, NULL, iter, FALSE);
		g_object_ref (context->priv->mark);
	}
	else
	{
		gtk_text_buffer_move_mark (buffer, context->priv->mark, iter);
	}

	g_object_notify (G_OBJECT (context), "iter");
}

static void
gtk_source_completion_context_set_property (GObject      *object,
                                            guint         prop_id,
                                            const GValue *value,
                                            GParamSpec   *pspec)
{
	GtkSourceCompletionContext *context = GTK_SOURCE_COMPLETION_CONTEXT (object);

	switch (prop_id)
	{
		case PROP_COMPLETION:
			context->priv->completion = g_value_dup_object (value);
			break;

		case PROP_ITER:
			set_iter (context, g_value_get_boxed (value));
			break;

		case PROP_ACTIVATION:
			context->priv->activation = g_value_get_flags (value);
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
	}
}

static void
gtk_source_completion_context_get_property (GObject    *object,
                                            guint       prop_id,
                                            GValue     *value,
                                            GParamSpec *pspec)
{
	GtkSourceCompletionContext *context = GTK_SOURCE_COMPLETION_CONTEXT (object);

	switch (prop_id)
	{
		case PROP_COMPLETION:
			g_value_set_object (value, context->priv->completion);
			break;

		case PROP_ITER:
			{
				GtkTextIter iter;

				if (gtk_source_completion_context_get_iter (context, &iter))
				{
					g_value_set_boxed (value, &iter);
				}
			}
			break;

		case PROP_ACTIVATION:
			g_value_set_flags (value, context->priv->activation);
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
	}
}

static void
gtk_source_completion_context_class_init (GtkSourceCompletionContextClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->set_property = gtk_source_completion_context_set_property;
	object_class->get_property = gtk_source_completion_context_get_property;
	object_class->dispose = gtk_source_completion_context_dispose;

	/**
	 * GtkSourceCompletionContext::cancelled:
	 *
	 * Emitted when the current population of proposals has been cancelled.
	 * Providers adding proposals asynchronously should connect to this signal
	 * to know when to cancel running proposal queries.
	 **/
	context_signals[CANCELLED] =
		g_signal_new ("cancelled",
		              G_TYPE_FROM_CLASS (klass),
		              G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
		              G_STRUCT_OFFSET (GtkSourceCompletionContextClass, cancelled),
		              NULL, NULL, NULL,
		              G_TYPE_NONE, 0);

	/**
	 * GtkSourceCompletionContext:completion:
	 *
	 * The #GtkSourceCompletion associated with the context.
	 **/
	g_object_class_install_property (object_class,
	                                 PROP_COMPLETION,
	                                 g_param_spec_object ("completion",
	                                                      "Completion",
	                                                      "The completion object to which the context belongs",
	                                                      GTK_SOURCE_TYPE_COMPLETION,
	                                                      G_PARAM_READWRITE |
							      G_PARAM_CONSTRUCT_ONLY |
							      G_PARAM_STATIC_STRINGS));

	/**
	 * GtkSourceCompletionContext:iter:
	 *
	 * The #GtkTextIter at which the completion is invoked.
	 **/
	g_object_class_install_property (object_class,
	                                 PROP_ITER,
					 g_param_spec_boxed ("iter",
							     "Iterator",
							     "The GtkTextIter at which the completion was invoked",
							     GTK_TYPE_TEXT_ITER,
							     G_PARAM_READWRITE |
							     G_PARAM_STATIC_STRINGS));

	/**
	 * GtkSourceCompletionContext:activation:
	 *
	 * The completion activation
	 **/
	g_object_class_install_property (object_class,
	                                 PROP_ACTIVATION,
	                                 g_param_spec_flags ("activation",
	                                                     "Activation",
	                                                     "The type of activation",
	                                                     GTK_SOURCE_TYPE_COMPLETION_ACTIVATION,
	                                                     GTK_SOURCE_COMPLETION_ACTIVATION_USER_REQUESTED,
	                                                     G_PARAM_READWRITE |
							     G_PARAM_CONSTRUCT |
							     G_PARAM_STATIC_STRINGS));
}

static void
gtk_source_completion_context_init (GtkSourceCompletionContext *context)
{
	context->priv = gtk_source_completion_context_get_instance_private (context);
}

/**
 * gtk_source_completion_context_add_proposals:
 * @context: a #GtkSourceCompletionContext.
 * @provider: a #GtkSourceCompletionProvider.
 * @proposals: (nullable) (element-type GtkSource.CompletionProposal): The list of proposals to add.
 * @finished: Whether the provider is finished adding proposals.
 *
 * Providers can use this function to add proposals to the completion. They
 * can do so asynchronously by means of the @finished argument. Providers must
 * ensure that they always call this function with @finished set to %TRUE
 * once each population (even if no proposals need to be added).
 * Population occurs when the gtk_source_completion_provider_populate()
 * function is called.
 **/
void
gtk_source_completion_context_add_proposals (GtkSourceCompletionContext  *context,
                                             GtkSourceCompletionProvider *provider,
                                             GList                       *proposals,
                                             gboolean                     finished)
{
	g_return_if_fail (GTK_SOURCE_IS_COMPLETION_CONTEXT (context));
	g_return_if_fail (GTK_SOURCE_IS_COMPLETION_PROVIDER (provider));

	_gtk_source_completion_add_proposals (context->priv->completion,
	                                      context,
	                                      provider,
	                                      proposals,
	                                      finished);
}

/**
 * gtk_source_completion_context_get_iter:
 * @context: a #GtkSourceCompletionContext.
 * @iter: (out): a #GtkTextIter.
 *
 * Get the iter at which the completion was invoked. Providers can use this
 * to determine how and if to match proposals.
 *
 * Returns: %TRUE if @iter is correctly set, %FALSE otherwise.
 **/
gboolean
gtk_source_completion_context_get_iter (GtkSourceCompletionContext *context,
                                        GtkTextIter                *iter)
{
	GtkTextBuffer *mark_buffer;
	GtkSourceView *view;
	GtkTextBuffer *completion_buffer;

	g_return_val_if_fail (GTK_SOURCE_IS_COMPLETION_CONTEXT (context), FALSE);

	if (context->priv->mark == NULL)
	{
		/* This should never happen: context should be always be created
		   providing a position iter */
		g_warning ("Completion context without mark");
		return FALSE;
	}

	mark_buffer = gtk_text_mark_get_buffer (context->priv->mark);

	if (mark_buffer == NULL)
	{
		return FALSE;
	}

	view = gtk_source_completion_get_view (context->priv->completion);
	if (view == NULL)
	{
		return FALSE;
	}

	completion_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));

	if (completion_buffer != mark_buffer)
	{
		return FALSE;
	}

	gtk_text_buffer_get_iter_at_mark (mark_buffer, iter, context->priv->mark);
	return TRUE;
}

/**
 * gtk_source_completion_context_get_activation:
 * @context: a #GtkSourceCompletionContext.
 *
 * Get the context activation.
 *
 * Returns: The context activation.
 */
GtkSourceCompletionActivation
gtk_source_completion_context_get_activation (GtkSourceCompletionContext *context)
{
	g_return_val_if_fail (GTK_SOURCE_IS_COMPLETION_CONTEXT (context),
			      GTK_SOURCE_COMPLETION_ACTIVATION_NONE);

	return context->priv->activation;
}

void
_gtk_source_completion_context_cancel (GtkSourceCompletionContext *context)
{
	g_return_if_fail (GTK_SOURCE_IS_COMPLETION_CONTEXT (context));

	g_signal_emit (context, context_signals[CANCELLED], 0);
}

GtkSourceCompletionContext *
_gtk_source_completion_context_new (GtkSourceCompletion *completion,
				    GtkTextIter         *position)
{
	g_return_val_if_fail (GTK_SOURCE_IS_COMPLETION (completion), NULL);
	g_return_val_if_fail (position != NULL, NULL);

	return g_object_new (GTK_SOURCE_TYPE_COMPLETION_CONTEXT,
	                     "completion", completion,
	                     "iter", position,
	                      NULL);
}