/* ide-completion.c * * Copyright 2018-2019 Christian Hergert * * 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-completion" #include "config.h" #include #include #include #include #include #include #include #ifdef GDK_WINDOWING_WAYLAND # include #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 "markup" and * the weight is set in the font description, the 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 <b> 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 = ""; static const gchar *end = ""; 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; } }