gem-graph-client/libide/sourceview/ide-completion.c

1925 lines
54 KiB
C
Raw Normal View History

/* ide-completion.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-completion"
#include "config.h"
#include <gtk/gtk.h>
#include <dazzle.h>
#include <gtksourceview/gtksource.h>
#include <libide-code.h>
#include <libide-plugins.h>
#include <libpeas/peas.h>
#include <string.h>
#ifdef GDK_WINDOWING_WAYLAND
# include <gdk/gdkwayland.h>
#endif
#include "ide-completion.h"
#include "ide-completion-context.h"
#include "ide-completion-display.h"
#include "ide-completion-overlay.h"
#include "ide-completion-private.h"
#include "ide-completion-proposal.h"
#include "ide-completion-provider.h"
#include "ide-source-view-private.h"
#define DEFAULT_N_ROWS 5
struct _IdeCompletion
{
GObject parent_instance;
/*
* The GtkSourceView that we are providing results for. This can be used by
* providers to get a reference.
*/
GtkSourceView *view;
/*
* A cancellable that we'll monitor to cancel anything that is currently in
* flight. This is reset to a new GCancellable after each time
* g_cancellable_cancel() is called.
*/
GCancellable *cancellable;
/*
* Our extension manager to get providers that were registered by plugins.
* We handle extension-added/extension-removed and add the results to the
* @providers array so that we can allow manual adding of providers too.
*/
IdeExtensionSetAdapter *addins;
/*
* An array of providers that have been registered. These will be queried
* when input is provided for completion.
*/
GPtrArray *providers;
/*
* If we are currently performing a completion, the context is stored here.
* It will be cleared as soon as it's no longer valid to (re)display.
*/
IdeCompletionContext *context;
/*
* The signal group is used to track changes to the context while it is our
* current context. That includes handling notification of the first result
* so that we can show the window, etc.
*/
DzlSignalGroup *context_signals;
/*
* Signals to changes in the underlying GtkTextBuffer that we use to
* determine where and how we can do completion.
*/
DzlSignalGroup *buffer_signals;
/*
* We need to track various events on the view to ensure that we don't
* activate at incorrect times.
*/
DzlSignalGroup *view_signals;
/*
* The display for results. This may use a different implementation based on
* the windowing system available to work around restrictions. For example,
* on wayland or quartz we'd use a toplevel GtkOverlay to draw into where as
* on Xorg we might just use an native window since we have more flexibility
* in Move/Resize there.
*/
IdeCompletionDisplay *display;
/*
* Our current event while processing so that we can get access to it
* from a callback back into the completion instance.
*/
const GdkEventKey *current_event;
/*
* Our cached font description to apply to views.
*/
PangoFontDescription *font_desc;
/*
* If we have a queued update to refilter after deletions, this will be
* set to the GSource id.
*/
guint queued_update;
/*
* This value is incremented/decremented based on if we need to suppress
* visibility of the completion window (and avoid doing queries).
*/
guint block_count;
/* Re-entrancy protection for ide_completion_show(). */
guint showing;
/*
* The number of rows to display. This is propagated to the window if/when
* the window is created.
*/
guint n_rows;
/* If we're currently being displayed */
guint shown : 1;
/* If we have a completion actively in play */
guint waiting_for_results : 1;
/* If we should refilter after the in-flight context completes */
guint needs_refilter : 1;
};
G_DEFINE_FINAL_TYPE (IdeCompletion, ide_completion, G_TYPE_OBJECT)
enum {
PROP_0,
PROP_BUFFER,
PROP_N_ROWS,
PROP_VIEW,
N_PROPS
};
enum {
ACTIVATE,
PROVIDER_ADDED,
PROVIDER_REMOVED,
SHOW,
HIDE,
N_SIGNALS
};
static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];
static gboolean
ide_completion_is_blocked (IdeCompletion *self)
{
GtkTextBuffer *buffer;
g_assert (IDE_IS_COMPLETION (self));
return self->block_count > 0 ||
self->view == NULL ||
self->providers->len == 0 ||
!gtk_widget_get_visible (GTK_WIDGET (self->view)) ||
!gtk_widget_has_focus (GTK_WIDGET (self->view)) ||
!(buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view))) ||
gtk_text_buffer_get_has_selection (buffer) ||
!IDE_IS_SOURCE_VIEW (self->view) ||
_ide_source_view_has_cursors (IDE_SOURCE_VIEW (self->view)) ||
!ide_source_view_is_processing_key (IDE_SOURCE_VIEW (self->view));
}
static void
ide_completion_complete_cb (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
IdeCompletionContext *context = (IdeCompletionContext *)object;
g_autoptr(IdeCompletion) self = user_data;
g_autoptr(GError) error = NULL;
IdeCompletionDisplay *display;
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION_CONTEXT (context));
g_assert (G_IS_ASYNC_RESULT (result));
g_assert (IDE_IS_COMPLETION (self));
if (self->context == context)
self->waiting_for_results = FALSE;
if (!_ide_completion_context_complete_finish (context, result, &error))
{
g_debug ("%s", error->message);
IDE_EXIT;
}
if (context != self->context)
IDE_EXIT;
if (self->needs_refilter)
{
/*
* At this point, we've gotten our new results for the context. But we had
* new content come in since we fired that request. So we need to ask the
* providers to further reduce the list based on updated query text.
*/
self->needs_refilter = FALSE;
_ide_completion_context_refilter (context);
}
display = ide_completion_get_display (self);
if (!ide_completion_context_is_empty (context))
gtk_widget_show (GTK_WIDGET (display));
else
gtk_widget_hide (GTK_WIDGET (display));
IDE_EXIT;
}
static void
ide_completion_set_context (IdeCompletion *self,
IdeCompletionContext *context)
{
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
g_assert (!context || IDE_IS_COMPLETION_CONTEXT (context));
if (g_set_object (&self->context, context))
dzl_signal_group_set_target (self->context_signals, context);
IDE_EXIT;
}
static inline gboolean
is_symbol_char (gunichar ch)
{
return ch == '_' || g_unichar_isalnum (ch);
}
static gboolean
ide_completion_compute_bounds (IdeCompletion *self,
GtkTextIter *begin,
GtkTextIter *end)
{
GtkTextBuffer *buffer;
GtkTextMark *insert;
gunichar ch = 0;
g_assert (IDE_IS_COMPLETION (self));
g_assert (begin != NULL);
g_assert (end != NULL);
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
insert = gtk_text_buffer_get_insert (buffer);
gtk_text_buffer_get_iter_at_mark (buffer, end, insert);
*begin = *end;
do
{
if (!gtk_text_iter_backward_char (begin))
break;
ch = gtk_text_iter_get_char (begin);
}
while (is_symbol_char (ch));
if (ch && !is_symbol_char (ch))
gtk_text_iter_forward_char (begin);
if (GTK_SOURCE_IS_BUFFER (buffer))
{
GtkSourceBuffer *gsb = GTK_SOURCE_BUFFER (buffer);
if (gtk_source_buffer_iter_has_context_class (gsb, begin, "comment") ||
gtk_source_buffer_iter_has_context_class (gsb, begin, "string") ||
gtk_source_buffer_iter_has_context_class (gsb, end, "comment") ||
gtk_source_buffer_iter_has_context_class (gsb, end, "string"))
return FALSE;
}
return !gtk_text_iter_equal (begin, end);
}
static void
ide_completion_start (IdeCompletion *self,
IdeCompletionActivation activation)
{
g_autoptr(IdeCompletionContext) context = NULL;
GtkTextIter begin;
GtkTextIter end;
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
g_assert (self->context == NULL);
dzl_clear_source (&self->queued_update);
if (!ide_completion_compute_bounds (self, &begin, &end))
{
if (activation == IDE_COMPLETION_INTERACTIVE)
IDE_EXIT;
begin = end;
}
context = _ide_completion_context_new (self);
for (guint i = 0; i < self->providers->len; i++)
_ide_completion_context_add_provider (context, g_ptr_array_index (self->providers, i));
ide_completion_set_context (self, context);
self->waiting_for_results = TRUE;
self->needs_refilter = FALSE;
_ide_completion_context_complete_async (context,
activation,
&begin,
&end,
self->cancellable,
ide_completion_complete_cb,
g_object_ref (self));
if (self->display != NULL)
{
ide_completion_display_set_context (self->display, context);
if (!ide_completion_context_is_empty (context))
gtk_widget_show (GTK_WIDGET (self->display));
else
gtk_widget_hide (GTK_WIDGET (self->display));
}
IDE_EXIT;
}
static void
ide_completion_update (IdeCompletion *self,
IdeCompletionActivation activation)
{
GtkTextBuffer *buffer;
GtkTextMark *insert;
GtkTextIter begin;
GtkTextIter end;
GtkTextIter iter;
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
g_assert (self->context != NULL);
g_assert (IDE_IS_COMPLETION_CONTEXT (self->context));
/*
* First, find the boundary for the word we are trying to complete. We might
* be able to refine a previous query instead of making a new one which can
* save on a lot of backend work.
*/
ide_completion_compute_bounds (self, &begin, &end);
if (_ide_completion_context_can_refilter (self->context, &begin, &end))
{
IdeCompletionDisplay *display = ide_completion_get_display (self);
/*
* Make sure we update providers that have already delivered results
* even though some of them won't be ready yet.
*/
_ide_completion_context_refilter (self->context);
/*
* If we're waiting for the results still to come in, then just mark
* that we need to do post-processing rather than trying to refilter now.
*/
if (self->waiting_for_results)
{
self->needs_refilter = TRUE;
IDE_EXIT;
}
if (!ide_completion_context_is_empty (self->context))
gtk_widget_show (GTK_WIDGET (display));
else
gtk_widget_hide (GTK_WIDGET (display));
IDE_EXIT;
}
if (!ide_completion_context_get_bounds (self->context, &begin, &end) ||
gtk_text_iter_equal (&begin, &end))
{
if (activation == IDE_COMPLETION_INTERACTIVE)
{
ide_completion_hide (self);
IDE_EXIT;
}
IDE_GOTO (reset);
}
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
insert = gtk_text_buffer_get_insert (buffer);
gtk_text_buffer_get_iter_at_mark (buffer, &iter, insert);
/*
* If our completion prefix bounds match the prefix that we looked
* at previously, we can possibly refilter the previous context instead
* of creating a new context.
*/
/*
* The context uses GtkTextMark which should have been advanced as
* the user continued to type. So if @end matches @iter (our insert
* location), then we can possibly update the previous context by
* further refining the query to a subset of the result.
*/
if (gtk_text_iter_equal (&iter, &end))
{
ide_completion_show (self);
IDE_EXIT;
}
reset:
ide_completion_cancel (self);
ide_completion_start (self, activation);
IDE_EXIT;
}
static void
ide_completion_real_hide (IdeCompletion *self)
{
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
if (self->display != NULL)
gtk_widget_hide (GTK_WIDGET (self->display));
IDE_EXIT;
}
static IdeCompletionDisplay *
ide_completion_create_display (IdeCompletion *self)
{
GtkWidget *widget = GTK_WIDGET (self->view);
GdkDisplay *display = gtk_widget_get_display (widget);
if (FALSE) {}
#ifdef GDK_WINDOWING_WAYLAND
else if (GDK_IS_WAYLAND_DISPLAY (display))
return IDE_COMPLETION_DISPLAY (_ide_completion_overlay_new ());
#endif
#ifdef GDK_WINDOWING_QUARTZ
/* Do string type check to avoid including obj-c header */
else if (g_strcmp0 ("GdkQuartzDisplay", G_OBJECT_TYPE_NAME (display)) == 0)
return IDE_COMPLETION_DISPLAY (_ide_completion_overlay_new ());
#endif
else
return IDE_COMPLETION_DISPLAY (_ide_completion_window_new (widget));
}
static void
ide_completion_real_show (IdeCompletion *self)
{
IdeCompletionDisplay *display;
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
display = ide_completion_get_display (self);
if (self->context == NULL)
ide_completion_start (self, IDE_COMPLETION_USER_REQUESTED);
else
ide_completion_update (self, IDE_COMPLETION_USER_REQUESTED);
ide_completion_display_set_context (display, self->context);
if (!ide_completion_context_is_empty (self->context))
gtk_widget_show (GTK_WIDGET (display));
else
gtk_widget_hide (GTK_WIDGET (display));
IDE_EXIT;
}
static void
ide_completion_notify_context_empty_cb (IdeCompletion *self,
GParamSpec *pspec,
IdeCompletionContext *context)
{
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
g_assert (pspec != NULL);
g_assert (IDE_IS_COMPLETION_CONTEXT (context));
if (context != self->context)
IDE_EXIT;
if (ide_completion_context_is_empty (context))
{
if (self->display != NULL)
gtk_widget_hide (GTK_WIDGET (self->display));
}
else
{
IdeCompletionDisplay *display = ide_completion_get_display (self);
gtk_widget_show (GTK_WIDGET (display));
}
IDE_EXIT;
}
static gboolean
ide_completion_view_button_press_event_cb (IdeCompletion *self,
GdkEventButton *event,
GtkSourceView *view)
{
g_assert (IDE_IS_COMPLETION (self));
g_assert (event != NULL);
g_assert (IDE_IS_SOURCE_VIEW (view));
g_assert (self->view == view);
ide_completion_hide (self);
return GDK_EVENT_PROPAGATE;
}
static gboolean
ide_completion_view_focus_out_event_cb (IdeCompletion *self,
GdkEventFocus *event,
GtkSourceView *view)
{
g_assert (IDE_IS_COMPLETION (self));
g_assert (event != NULL);
g_assert (IDE_IS_SOURCE_VIEW (view));
g_assert (self->view == view);
ide_completion_hide (self);
return GDK_EVENT_PROPAGATE;
}
static gboolean
ide_completion_view_key_press_event_cb (IdeCompletion *self,
GdkEventKey *event,
GtkSourceView *view)
{
GtkBindingSet *binding_set;
gboolean ret = GDK_EVENT_PROPAGATE;
g_assert (IDE_IS_COMPLETION (self));
g_assert (event != NULL);
g_assert (event->type == GDK_KEY_PRESS);
g_assert (IDE_IS_SOURCE_VIEW (view));
g_assert (self->view == view);
binding_set = gtk_binding_set_by_class (G_OBJECT_GET_CLASS (self));
self->current_event = event;
if (self->display != NULL &&
gtk_widget_get_visible (GTK_WIDGET (self->display)) &&
ide_completion_display_key_press_event (self->display, event))
ret = GDK_EVENT_STOP;
self->current_event = NULL;
if (ret == GDK_EVENT_PROPAGATE)
ret = gtk_binding_set_activate (binding_set, event->keyval, event->state, G_OBJECT (self));
return ret;
}
static void
ide_completion_view_move_cursor_cb (IdeCompletion *self,
GtkMovementStep step,
gint count,
gboolean extend_selection,
GtkSourceView *view)
{
g_assert (IDE_IS_COMPLETION (self));
g_assert (IDE_IS_SOURCE_VIEW (view));
/* TODO: Should we keep the context alive while we begin a new one?
* Or rather, how can we avoid the hide/show of the widget that
* could result in flicker?
*/
if (self->display != NULL &&
gtk_widget_get_visible (GTK_WIDGET (self->display)))
ide_completion_cancel (self);
}
static gboolean
ide_completion_queued_update_cb (gpointer user_data)
{
IdeCompletion *self = user_data;
g_assert (IDE_IS_COMPLETION (self));
self->queued_update = 0;
if (self->context != NULL)
ide_completion_update (self, IDE_COMPLETION_INTERACTIVE);
return G_SOURCE_REMOVE;
}
static void
ide_completion_queue_update (IdeCompletion *self)
{
g_assert (IDE_IS_COMPLETION (self));
dzl_clear_source (&self->queued_update);
/*
* We hit this code path when the user has deleted text. We want to
* introduce just a bit of delay so that deleting under heavy key
* repeat will not stall doing lots of refiltering.
*/
self->queued_update =
gdk_threads_add_timeout_full (G_PRIORITY_LOW,
20,
ide_completion_queued_update_cb,
self,
NULL);
}
static void
ide_completion_buffer_delete_range_after_cb (IdeCompletion *self,
GtkTextIter *begin,
GtkTextIter *end,
GtkTextBuffer *buffer)
{
g_assert (IDE_IS_COMPLETION (self));
g_assert (IDE_IS_SOURCE_VIEW (self->view));
g_assert (begin != NULL);
g_assert (end != NULL);
g_assert (GTK_IS_TEXT_BUFFER (buffer));
if (self->context != NULL)
{
if (!ide_completion_is_blocked (self))
{
GtkTextIter b, e;
ide_completion_context_get_bounds (self->context, &b, &e);
/*
* If they just backspaced all of the text, then we want to just hide
* the completion window since that can get a bit intrusive.
*/
if (gtk_text_iter_equal (&b, &e))
{
dzl_clear_source (&self->queued_update);
ide_completion_hide (self);
return;
}
ide_completion_queue_update (self);
}
}
}
static gboolean
is_single_char (const gchar *text,
gint len)
{
if (len == 1)
return TRUE;
else if (len > 6)
return FALSE;
else
return g_utf8_strlen (text, len) == 1;
}
static void
ide_completion_buffer_insert_text_after_cb (IdeCompletion *self,
GtkTextIter *iter,
const gchar *text,
gint len,
GtkTextBuffer *buffer)
{
IdeCompletionActivation activation = IDE_COMPLETION_INTERACTIVE;
GtkTextIter begin;
GtkTextIter end;
g_assert (IDE_IS_COMPLETION (self));
g_assert (iter != NULL);
g_assert (text != NULL);
g_assert (len > 0);
g_assert (GTK_IS_TEXT_BUFFER (buffer));
if (ide_buffer_get_loading (IDE_BUFFER (buffer)))
return;
dzl_clear_source (&self->queued_update);
if (ide_completion_is_blocked (self) || !is_single_char (text, len))
{
ide_completion_cancel (self);
return;
}
if (!ide_completion_compute_bounds (self, &begin, &end))
{
GtkTextIter cur = end;
if (gtk_text_iter_backward_char (&cur))
{
gunichar ch = gtk_text_iter_get_char (&cur);
for (guint i = 0; i < self->providers->len; i++)
{
IdeCompletionProvider *provider = g_ptr_array_index (self->providers, i);
if (ide_completion_provider_is_trigger (provider, &end, ch))
{
/*
* We got a trigger, but we failed to continue the bounds of a previous
* completion. We need to cancel the previous completion (if any) first
* and then try to start a new completion due to trigger.
*/
ide_completion_cancel (self);
activation = IDE_COMPLETION_TRIGGERED;
goto do_completion;
}
}
}
ide_completion_cancel (self);
return;
}
do_completion:
if (self->context == NULL)
ide_completion_start (self, activation);
else
ide_completion_update (self, activation);
}
static void
ide_completion_buffer_mark_set_cb (IdeCompletion *self,
const GtkTextIter *iter,
GtkTextMark *mark,
GtkTextBuffer *buffer)
{
g_assert (IDE_IS_COMPLETION (self));
g_assert (GTK_IS_TEXT_MARK (mark));
g_assert (GTK_IS_TEXT_BUFFER (buffer));
if (mark != gtk_text_buffer_get_insert (buffer))
return;
if (_ide_completion_context_iter_invalidates (self->context, iter))
ide_completion_cancel (self);
}
static void
ide_completion_set_view (IdeCompletion *self,
GtkSourceView *view)
{
g_assert (IDE_IS_COMPLETION (self));
g_assert (!view || IDE_IS_SOURCE_VIEW (view));
if (view == NULL)
{
g_critical ("%s created without a view", G_OBJECT_TYPE_NAME (self));
return;
}
if (g_set_weak_pointer (&self->view, view))
{
dzl_signal_group_set_target (self->view_signals, view);
g_object_bind_property (view, "buffer",
self->buffer_signals, "target",
G_BINDING_SYNC_CREATE);
}
}
static void
ide_completion_addins_extension_added_cb (IdeExtensionSetAdapter *adapter,
PeasPluginInfo *plugin_info,
PeasExtension *exten,
gpointer user_data)
{
IdeCompletionProvider *provider = (IdeCompletionProvider *)exten;
IdeCompletion *self = user_data;
GtkTextBuffer *buffer;
IDE_ENTRY;
g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
g_assert (plugin_info != NULL);
g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
if ((buffer = ide_completion_get_buffer (self)) && IDE_IS_BUFFER (buffer))
{
g_autoptr(IdeContext) context = ide_buffer_ref_context (IDE_BUFFER (buffer));
_ide_completion_provider_load (provider, context);
}
ide_completion_add_provider (self, provider);
IDE_EXIT;
}
static void
ide_completion_addins_extension_removed_cb (IdeExtensionSetAdapter *adapter,
PeasPluginInfo *plugin_info,
PeasExtension *exten,
gpointer user_data)
{
IdeCompletionProvider *provider = (IdeCompletionProvider *)exten;
IdeCompletion *self = user_data;
IDE_ENTRY;
g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
g_assert (plugin_info != NULL);
g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
ide_completion_remove_provider (self, provider);
IDE_EXIT;
}
static void
ide_completion_buffer_signals_bind_cb (IdeCompletion *self,
GtkSourceBuffer *buffer,
DzlSignalGroup *group)
{
GtkSourceLanguage *language;
IdeObjectBox *box;
const gchar *language_id = NULL;
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
g_assert (GTK_SOURCE_IS_BUFFER (buffer));
g_assert (DZL_IS_SIGNAL_GROUP (group));
if (!IDE_IS_BUFFER (buffer))
return;
if ((language = gtk_source_buffer_get_language (buffer)))
language_id = gtk_source_language_get_id (language);
box = ide_object_box_from_object (G_OBJECT (buffer));
self->addins = ide_extension_set_adapter_new (IDE_OBJECT (box),
peas_engine_get_default (),
IDE_TYPE_COMPLETION_PROVIDER,
"Completion-Provider-Languages",
language_id);
g_signal_connect_object (self->addins,
"extension-added",
G_CALLBACK (ide_completion_addins_extension_added_cb),
self, 0);
g_signal_connect_object (self->addins,
"extension-removed",
G_CALLBACK (ide_completion_addins_extension_removed_cb),
self, 0);
ide_extension_set_adapter_foreach (self->addins,
ide_completion_addins_extension_added_cb,
self);
IDE_EXIT;
}
static void
ide_completion_buffer_signals_unbind_cb (IdeCompletion *self,
DzlSignalGroup *group)
{
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
g_assert (DZL_IS_SIGNAL_GROUP (group));
ide_clear_and_destroy_object (&self->addins);
IDE_EXIT;
}
static void
ide_completion_buffer_notify_language_cb (IdeCompletion *self,
GParamSpec *pspec,
GtkSourceBuffer *buffer)
{
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
g_assert (pspec != NULL);
g_assert (GTK_SOURCE_IS_BUFFER (buffer));
if (self->addins != NULL)
{
GtkSourceLanguage *language;
const gchar *language_id = NULL;
if ((language = gtk_source_buffer_get_language (buffer)))
language_id = gtk_source_language_get_id (language);
ide_extension_set_adapter_set_value (self->addins, language_id);
}
IDE_EXIT;
}
static void
ide_completion_dispose (GObject *object)
{
IdeCompletion *self = (IdeCompletion *)object;
IDE_ENTRY;
g_assert (IDE_IS_COMPLETION (self));
if (self->display != NULL)
gtk_widget_destroy (GTK_WIDGET (self->display));
g_assert (self->display == NULL);
dzl_signal_group_set_target (self->context_signals, NULL);
dzl_signal_group_set_target (self->buffer_signals, NULL);
dzl_signal_group_set_target (self->view_signals, NULL);
g_clear_object (&self->context);
g_clear_object (&self->cancellable);
if (self->providers->len > 0)
g_ptr_array_remove_range (self->providers, 0, self->providers->len);
G_OBJECT_CLASS (ide_completion_parent_class)->dispose (object);
IDE_EXIT;
}
static void
ide_completion_finalize (GObject *object)
{
IdeCompletion *self = (IdeCompletion *)object;
IDE_ENTRY;
dzl_clear_source (&self->queued_update);
g_clear_object (&self->cancellable);
ide_clear_and_destroy_object (&self->addins);
g_clear_object (&self->buffer_signals);
g_clear_object (&self->context_signals);
g_clear_object (&self->view_signals);
g_clear_object (&self->context);
g_clear_object (&self->cancellable);
g_clear_pointer (&self->providers, g_ptr_array_unref);
g_clear_pointer (&self->font_desc, pango_font_description_free);
g_clear_weak_pointer (&self->view);
G_OBJECT_CLASS (ide_completion_parent_class)->finalize (object);
IDE_EXIT;
}
static void
ide_completion_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
IdeCompletion *self = IDE_COMPLETION (object);
switch (prop_id)
{
case PROP_BUFFER:
g_value_set_object (value, ide_completion_get_buffer (self));
break;
case PROP_N_ROWS:
g_value_set_uint (value, ide_completion_get_n_rows (self));
break;
case PROP_VIEW:
g_value_set_object (value, self->view);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_completion_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
IdeCompletion *self = IDE_COMPLETION (object);
switch (prop_id)
{
case PROP_N_ROWS:
ide_completion_set_n_rows (self, g_value_get_uint (value));
break;
case PROP_VIEW:
ide_completion_set_view (self, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_completion_class_init (IdeCompletionClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkBindingSet *binding_set;
object_class->dispose = ide_completion_dispose;
object_class->finalize = ide_completion_finalize;
object_class->get_property = ide_completion_get_property;
object_class->set_property = ide_completion_set_property;
/**
* IdeCompletion:buffer:
*
* The #GtkTextBuffer for the #IdeCompletion:view.
* This is a convenience property for providers.
*
* Since: 3.32
*/
properties [PROP_BUFFER] =
g_param_spec_object ("buffer",
"Buffer",
"The buffer for the view",
GTK_TYPE_TEXT_VIEW,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
/**
* IdeCompletion:n-rows:
*
* The number of rows to display to the user.
*
* Since: 3.32
*/
properties [PROP_N_ROWS] =
g_param_spec_uint ("n-rows",
"Number of Rows",
"Number of rows to display to the user",
1, 32, DEFAULT_N_ROWS,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* IdeCompletion:view:
*
* The "view" property is the #GtkTextView for which this #IdeCompletion
* is providing completion features.
*
* Since: 3.32
*/
properties [PROP_VIEW] =
g_param_spec_object ("view",
"View",
"The text view for which to provide completion",
GTK_SOURCE_TYPE_VIEW,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPS, properties);
/**
* IdeCompletion::provider-added:
* @self: an #ideCompletion
* @provider: an #IdeCompletionProvider
*
* The "provided-added" signal is emitted when a new provider is
* added to the completion.
*
* Since: 3.32
*/
signals [PROVIDER_ADDED] =
g_signal_new ("provider-added",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0, NULL, NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE, 1, IDE_TYPE_COMPLETION_PROVIDER);
g_signal_set_va_marshaller (signals [PROVIDER_ADDED],
G_TYPE_FROM_CLASS (klass),
g_cclosure_marshal_VOID__OBJECTv);
/**
* IdeCompletion::provider-removed:
* @self: an #ideCompletion
* @provider: an #IdeCompletionProvider
*
* The "provided-removed" signal is emitted when a provider has
* been removed from the completion.
*
* Since: 3.32
*/
signals [PROVIDER_REMOVED] =
g_signal_new ("provider-removed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0, NULL, NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE, 1, IDE_TYPE_COMPLETION_PROVIDER);
g_signal_set_va_marshaller (signals [PROVIDER_REMOVED],
G_TYPE_FROM_CLASS (klass),
g_cclosure_marshal_VOID__OBJECTv);
/**
* IdeCompletion::hide:
* @self: an #IdeCompletion
*
* The "hide" signal is emitted when the completion window should
* be hidden.
*
* Since: 3.32
*/
signals [HIDE] =
g_signal_new_class_handler ("hide",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (ide_completion_real_hide),
NULL, NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
g_signal_set_va_marshaller (signals [HIDE],
G_TYPE_FROM_CLASS (klass),
g_cclosure_marshal_VOID__VOIDv);
/**
* IdeCompletion::show:
* @self: an #IdeCompletion
*
* The "show" signal is emitted when the completion window should
* be shown.
*
* Since: 3.32
*/
signals [SHOW] =
g_signal_new_class_handler ("show",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (ide_completion_real_show),
NULL, NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
g_signal_set_va_marshaller (signals [SHOW],
G_TYPE_FROM_CLASS (klass),
g_cclosure_marshal_VOID__VOIDv);
binding_set = gtk_binding_set_by_class (klass);
gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, GDK_CONTROL_MASK, "show", 0);
}
static void
ide_completion_init (IdeCompletion *self)
{
self->cancellable = g_cancellable_new ();
self->providers = g_ptr_array_new_with_free_func (g_object_unref);
self->buffer_signals = dzl_signal_group_new (GTK_TYPE_TEXT_BUFFER);
self->context_signals = dzl_signal_group_new (IDE_TYPE_COMPLETION_CONTEXT);
self->view_signals = dzl_signal_group_new (GTK_SOURCE_TYPE_VIEW);
self->n_rows = DEFAULT_N_ROWS;
/*
* We want to be notified when the context switches from no results to
* having results (or vice-versa, when we've filtered to the point of
* no results).
*/
dzl_signal_group_connect_object (self->context_signals,
"notify::empty",
G_CALLBACK (ide_completion_notify_context_empty_cb),
self,
G_CONNECT_SWAPPED);
/*
* We need to know when the buffer inserts or deletes text so that we
* possibly start showing the results, or update our previous completion
* request.
*/
g_signal_connect_object (self->buffer_signals,
"bind",
G_CALLBACK (ide_completion_buffer_signals_bind_cb),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->buffer_signals,
"unbind",
G_CALLBACK (ide_completion_buffer_signals_unbind_cb),
self,
G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->buffer_signals,
"notify::language",
G_CALLBACK (ide_completion_buffer_notify_language_cb),
self,
G_CONNECT_AFTER | G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->buffer_signals,
"delete-range",
G_CALLBACK (ide_completion_buffer_delete_range_after_cb),
self,
G_CONNECT_AFTER | G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->buffer_signals,
"insert-text",
G_CALLBACK (ide_completion_buffer_insert_text_after_cb),
self,
G_CONNECT_AFTER | G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->buffer_signals,
"mark-set",
G_CALLBACK (ide_completion_buffer_mark_set_cb),
self,
G_CONNECT_SWAPPED);
/*
* We track some events on the view that owns our IdeCompletion instance so
* that we can hide the window when it definitely should not be displayed.
*/
dzl_signal_group_connect_object (self->view_signals,
"button-press-event",
G_CALLBACK (ide_completion_view_button_press_event_cb),
self,
G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->view_signals,
"focus-out-event",
G_CALLBACK (ide_completion_view_focus_out_event_cb),
self,
G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->view_signals,
"key-press-event",
G_CALLBACK (ide_completion_view_key_press_event_cb),
self,
G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->view_signals,
"move-cursor",
G_CALLBACK (ide_completion_view_move_cursor_cb),
self,
G_CONNECT_AFTER | G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->view_signals,
"paste-clipboard",
G_CALLBACK (ide_completion_block_interactive),
self,
G_CONNECT_SWAPPED);
dzl_signal_group_connect_object (self->view_signals,
"paste-clipboard",
G_CALLBACK (ide_completion_unblock_interactive),
self,
G_CONNECT_AFTER | G_CONNECT_SWAPPED);
}
/**
* ide_completion_get_view:
* @self: a #IdeCompletion
*
* Returns: (transfer none): an #GtkSourceView
*
* Since: 3.32
*/
GtkSourceView *
ide_completion_get_view (IdeCompletion *self)
{
g_return_val_if_fail (IDE_IS_COMPLETION (self), NULL);
return self->view;
}
/**
* ide_completion_get_buffer:
* @self: a #IdeCompletion
*
* Returns: (transfer none): a #GtkTextBuffer
*
* Since: 3.32
*/
GtkTextBuffer *
ide_completion_get_buffer (IdeCompletion *self)
{
g_return_val_if_fail (IDE_IS_COMPLETION (self), NULL);
return gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
}
IdeCompletion *
_ide_completion_new (GtkSourceView *view)
{
g_return_val_if_fail (IDE_IS_SOURCE_VIEW (view), NULL);
return g_object_new (IDE_TYPE_COMPLETION,
"view", view,
NULL);
}
/**
* ide_completion_add_provider:
* @self: an #IdeCompletion
* @provider: an #IdeCompletionProvider
*
* Adds an #IdeCompletionProvider to the list of providers to be queried
* for completion results.
*
* Since: 3.32
*/
void
ide_completion_add_provider (IdeCompletion *self,
IdeCompletionProvider *provider)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (provider));
g_ptr_array_add (self->providers, g_object_ref (provider));
g_signal_emit (self, signals [PROVIDER_ADDED], 0, provider);
IDE_EXIT;
}
/**
* ide_completion_remove_provider:
* @self: an #IdeCompletion
* @provider: an #IdeCompletionProvider
*
* Removes an #IdeCompletionProvider previously added with
* ide_completion_add_provider().
*
* Since: 3.32
*/
void
ide_completion_remove_provider (IdeCompletion *self,
IdeCompletionProvider *provider)
{
g_autoptr(IdeCompletionProvider) hold = NULL;
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (provider));
hold = g_object_ref (provider);
if (g_ptr_array_remove (self->providers, provider))
g_signal_emit (self, signals [PROVIDER_REMOVED], 0, hold);
IDE_EXIT;
}
/**
* ide_completion_show:
* @self: an #IdeCompletion
*
* Emits the "show" signal.
*
* When the "show" signal is emitted, the completion window will be
* displayed if there are any results available.
*
* Since: 3.32
*/
void
ide_completion_show (IdeCompletion *self)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
if (ide_completion_is_blocked (self))
IDE_EXIT;
self->showing++;
if (self->showing == 1)
g_signal_emit (self, signals [SHOW], 0);
self->showing--;
IDE_EXIT;
}
/**
* ide_completion_hide:
* @self: an #IdeCompletion
*
* Emits the "hide" signal.
*
* When the "hide" signal is emitted, the completion window will be
* dismissed.
*
* Since: 3.32
*/
void
ide_completion_hide (IdeCompletion *self)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
g_signal_emit (self, signals [HIDE], 0);
IDE_EXIT;
}
void
ide_completion_cancel (IdeCompletion *self)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
/* Nothing can re-use in-flight results now */
self->waiting_for_results = FALSE;
self->needs_refilter = FALSE;
if (self->context != NULL)
{
g_cancellable_cancel (self->cancellable);
g_clear_object (&self->cancellable);
ide_completion_set_context (self, NULL);
if (self->display != NULL)
{
ide_completion_display_set_context (self->display, NULL);
gtk_widget_hide (GTK_WIDGET (self->display));
}
}
IDE_EXIT;
}
void
ide_completion_block_interactive (IdeCompletion *self)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
self->block_count++;
ide_completion_cancel (self);
IDE_EXIT;
}
void
ide_completion_unblock_interactive (IdeCompletion *self)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
self->block_count--;
IDE_EXIT;
}
void
ide_completion_set_n_rows (IdeCompletion *self,
guint n_rows)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
g_return_if_fail (n_rows > 0);
g_return_if_fail (n_rows <= 32);
if (self->n_rows != n_rows)
{
self->n_rows = n_rows;
if (self->display != NULL)
ide_completion_display_set_n_rows (self->display, n_rows);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_N_ROWS]);
}
IDE_EXIT;
}
guint
ide_completion_get_n_rows (IdeCompletion *self)
{
g_return_val_if_fail (IDE_IS_COMPLETION (self), 0);
return self->n_rows;
}
void
_ide_completion_activate (IdeCompletion *self,
IdeCompletionContext *context,
IdeCompletionProvider *provider,
IdeCompletionProposal *proposal)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (context));
g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (provider));
g_return_if_fail (IDE_IS_COMPLETION_PROPOSAL (proposal));
g_debug ("Activating %s", G_OBJECT_TYPE_NAME (proposal));
self->block_count++;
ide_completion_provider_activate_poposal (provider, context, proposal, self->current_event);
self->block_count--;
IDE_EXIT;
}
void
_ide_completion_set_language_id (IdeCompletion *self,
const gchar *language_id)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
g_return_if_fail (language_id != NULL);
ide_extension_set_adapter_set_value (self->addins, language_id);
IDE_EXIT;
}
/**
* ide_completion_is_visible:
* @self: a #IdeCompletion
*
* Checks if the completion display is visible.
*
* Returns: %TRUE if the display is visible
*
* Since: 3.32
*/
gboolean
ide_completion_is_visible (IdeCompletion *self)
{
g_return_val_if_fail (IDE_IS_COMPLETION (self), FALSE);
if (self->display != NULL)
return gtk_widget_get_visible (GTK_WIDGET (self->display));
return FALSE;
}
/**
* ide_completion_get_display:
* @self: a #IdeCompletion
*
* Gets the display for completion.
*
* Returns: (transfer none): an #IdeCompletionDisplay
*
* Since: 3.32
*/
IdeCompletionDisplay *
ide_completion_get_display (IdeCompletion *self)
{
g_return_val_if_fail (IDE_IS_COMPLETION (self), NULL);
if (self->display == NULL)
{
self->display = ide_completion_create_display (self);
g_signal_connect (self->display,
"destroy",
G_CALLBACK (gtk_widget_destroyed),
&self->display);
ide_completion_display_set_n_rows (self->display, self->n_rows);
ide_completion_display_attach (self->display, self->view);
_ide_completion_display_set_font_desc (self->display, self->font_desc);
ide_completion_display_set_context (self->display, self->context);
}
return self->display;
}
void
ide_completion_move_cursor (IdeCompletion *self,
GtkMovementStep step,
gint direction)
{
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPLETION (self));
if (self->display != NULL)
ide_completion_display_move_cursor (self->display, step, direction);
IDE_EXIT;
}
void
_ide_completion_set_font_description (IdeCompletion *self,
const PangoFontDescription *font_desc)
{
g_return_if_fail (IDE_IS_COMPLETION (self));
if (font_desc != self->font_desc)
{
pango_font_description_free (self->font_desc);
self->font_desc = pango_font_description_copy (font_desc);
/*
* Work around issue where when a proposal provides "<b>markup</b>" and
* the weight is set in the font description, the <b> markup will not
* have it's weight respected. This seems to be happening because the
* weight mask is getting set in pango_font_description_from_string()
* even if the the value is set to normal. That matter is complicated
* because PangoAttrFontDesc and PangoAttrWeight will both have the
* same starting offset in the PangoLayout.
*
* https://bugzilla.gnome.org/show_bug.cgi?id=755968
*/
if (PANGO_WEIGHT_NORMAL == pango_font_description_get_weight (self->font_desc))
pango_font_description_unset_fields (self->font_desc, PANGO_FONT_MASK_WEIGHT);
if (self->display != NULL)
_ide_completion_display_set_font_desc (self->display, font_desc);
}
}
/**
* ide_completion_fuzzy_match:
* @haystack: (nullable): the string to be searched.
* @casefold_needle: A g_utf8_casefold() version of the needle.
* @priority: (out) (allow-none): An optional location for the score of the match
*
* This helper function can do a fuzzy match for you giving a haystack and
* casefolded needle. Casefold your needle using g_utf8_casefold() before
* running the query.
*
* Score will be set with the score of the match upon success. Otherwise,
* it will be set to zero.
*
* Returns: %TRUE if @haystack matched @casefold_needle, otherwise %FALSE.
*
* Since: 3.32
*/
gboolean
ide_completion_fuzzy_match (const gchar *haystack,
const gchar *casefold_needle,
guint *priority)
{
gint real_score = 0;
if (haystack == NULL || haystack[0] == 0)
return FALSE;
for (; *casefold_needle; casefold_needle = g_utf8_next_char (casefold_needle))
{
gunichar ch = g_utf8_get_char (casefold_needle);
gunichar chup = g_unichar_toupper (ch);
const gchar *tmp;
const gchar *downtmp;
const gchar *uptmp;
/*
* Note that the following code is not really correct. We want
* to be relatively fast here, but we also don't want to convert
* strings to casefolded versions for querying on each compare.
* So we use the casefold version and compare with upper. This
* works relatively well since we are usually dealing with ASCII
* for function names and symbols.
*/
downtmp = strchr (haystack, ch);
uptmp = strchr (haystack, chup);
if (downtmp && uptmp)
tmp = MIN (downtmp, uptmp);
else if (downtmp)
tmp = downtmp;
else if (uptmp)
tmp = uptmp;
else
return FALSE;
/*
* Here we calculate the cost of this character into the score.
* If we matched exactly on the next character, the cost is ZERO.
* However, if we had to skip some characters, we have a cost
* of 2*distance to the character. This is necessary so that
* when we add the cost of the remaining haystack, strings which
* exhausted @casefold_needle score lower (higher priority) than
* strings which had to skip characters but matched the same
* number of characters in the string.
*/
real_score += (tmp - haystack) * 2;
/* Add extra cost if we matched by using toupper */
if (*haystack == chup)
real_score += 1;
/*
* Now move past our matching character so we cannot match
* it a second time.
*/
haystack = tmp + 1;
}
if (priority != NULL)
*priority = real_score + strlen (haystack);
return TRUE;
}
/**
* ide_completion_fuzzy_highlight:
* @haystack: the string to be highlighted
* @casefold_query: the typed-text used to highlight @haystack
*
* This will add &lt;b&gt; tags around matched characters in @haystack
* based on @casefold_query.
*
* Returns: a newly allocated string
*
* Since: 3.32
*/
gchar *
ide_completion_fuzzy_highlight (const gchar *haystack,
const gchar *casefold_query)
{
static const gchar *begin = "<b>";
static const gchar *end = "</b>";
GString *ret;
gunichar str_ch;
gunichar match_ch;
gboolean element_open = FALSE;
if (haystack == NULL || casefold_query == NULL)
return g_strdup (haystack);
ret = g_string_new (NULL);
for (; *haystack; haystack = g_utf8_next_char (haystack))
{
str_ch = g_utf8_get_char (haystack);
match_ch = g_utf8_get_char (casefold_query);
if ((str_ch == match_ch) || (g_unichar_tolower (str_ch) == g_unichar_tolower (match_ch)))
{
if (!element_open)
{
g_string_append (ret, begin);
element_open = TRUE;
}
g_string_append_unichar (ret, str_ch);
/* TODO: We could seek to the next char and append in a batch. */
casefold_query = g_utf8_next_char (casefold_query);
}
else
{
if (element_open)
{
g_string_append (ret, end);
element_open = FALSE;
}
g_string_append_unichar (ret, str_ch);
}
}
if (element_open)
g_string_append (ret, end);
return g_string_free (ret, FALSE);
}
static gboolean
find_prefix_match (const GtkTextIter *limit,
const GtkTextIter *end,
GtkTextIter *found_start,
GtkTextIter *found_end,
const char *prefix,
gsize len,
gsize n_chars)
{
g_autofree gchar *copy = g_utf8_substring (prefix, 0, n_chars);
GtkTextIter mb, me;
if (gtk_text_iter_backward_search (end, copy, GTK_TEXT_SEARCH_TEXT_ONLY, &mb, &me, limit))
{
if (gtk_text_iter_equal (&me, end))
{
*found_start = mb;
*found_end = me;
return TRUE;
}
}
return FALSE;
}
void
ide_completion_remove_common_prefix (IdeCompletion *self,
GtkTextIter *begin,
const gchar *prefix)
{
GtkTextIter rm_begin;
GtkTextIter rm_end;
GtkTextIter line_start;
GtkTextIter found_start, found_end;
gboolean found = FALSE;
gsize len;
gsize count = 1;
g_return_if_fail (IDE_IS_COMPLETION (self));
g_return_if_fail (self->context != NULL);
g_return_if_fail (begin != NULL);
if (prefix == NULL || prefix[0] == 0)
return;
len = g_utf8_strlen (prefix, -1);
line_start = *begin;
gtk_text_iter_set_line_offset (&line_start, 0);
while (count <= len &&
find_prefix_match (&line_start, begin, &found_start, &found_end, prefix, len, count))
{
rm_begin = found_start;
rm_end = found_end;
count++;
found = TRUE;
}
if (found)
{
gtk_text_buffer_delete (gtk_text_iter_get_buffer (begin), &rm_begin, &rm_end);
*begin = rm_begin;
}
}
static gboolean
find_suffix_match (const GtkTextIter *iter,
const GtkTextIter *end,
GtkTextIter *found_start,
GtkTextIter *found_end,
const char *suffix,
gsize len,
gsize n_chars)
{
g_autofree gchar *copy = g_utf8_substring (suffix, len - n_chars, len);
GtkTextIter mb, me;
if (gtk_text_iter_forward_search (iter, copy, GTK_TEXT_SEARCH_TEXT_ONLY, &mb, &me, end))
{
if (gtk_text_iter_equal (&mb, iter))
{
*found_start = mb;
*found_end = me;
return TRUE;
}
}
return FALSE;
}
void
ide_completion_remove_common_suffix (IdeCompletion *self,
GtkTextIter *iter,
const gchar *suffix)
{
GtkTextIter rm_begin;
GtkTextIter rm_end;
GtkTextIter line_end;
GtkTextIter found_start, found_end;
gboolean found = FALSE;
gsize len;
gsize count = 1;
g_return_if_fail (IDE_IS_COMPLETION (self));
g_return_if_fail (self->context != NULL);
g_return_if_fail (iter != NULL);
if (suffix == NULL || suffix[0] == 0)
return;
len = g_utf8_strlen (suffix, -1);
line_end = *iter;
if (gtk_text_iter_ends_line (&line_end))
return;
gtk_text_iter_forward_to_line_end (&line_end);
while (count <= len &&
find_suffix_match (iter, &line_end, &found_start, &found_end, suffix, len, count))
{
rm_begin = found_start;
rm_end = found_end;
count++;
found = TRUE;
}
if (found)
{
gtk_text_buffer_delete (gtk_text_iter_get_buffer (iter), &rm_begin, &rm_end);
*iter = rm_begin;
}
}