Commit 3cd417a3 authored by Christian Hergert's avatar Christian Hergert

completion: add new completion engine

This is a replacement for GtkSourceCompletion to handle a number of designs
that we can't land into GtkSourceView for various API/ABI reasons but need
in Builder for completion performance, memory footprint, and visual
styling.

The important details in terms of design change are:

 * Use GListModel heavily to avoid GObject creation, GList creation,
   GQueue creation, and O(n²) treemodel lookups so that we can handle
   larger list sizes in (tens of thousands with much reduced memory
   footprint).
 * Allow providers to refilter existing results conveniently.
 * Break things into 4 columns (icon, lhs, word, rhs).
 * Use GtkListBoxRow instead of GtkTreeView for widget styling.
 * Only create widgets and proposals for the visible range of items
   through use of custom GtkBox-based GtkScrollable, listbox row, and
   dynamic access to joined-GListModel for all results.
 * Dynamic priorities for completion results based on context.
 * Adjust window size based on result set, with changeable xoffset.
 * Use different strategies for display based on our display manager.
 * Consistent and fast fuzzy text filtering.

Some things we don't have yet that we'd like to add in the future.

 * Support for more complex comments than fit in the completion window
   such as a details window.
 * Use move_to_rect() API landing in gtk+ 3.24.
 * When wayland supports dynamic repositioning of popups, use that with
   the IdeCompletionWindow instead of our overlay display.
 * Simplified support for parameter completion
 * Simplified support for string interpolation
parent 0abb01bf
......@@ -115,5 +115,11 @@
<summary>Autosave Frequency</summary>
<description>The number of seconds after modification before auto saving.</description>
</key>
<key name="completion-n-rows" type="i">
<default>7</default>
<range min="1" max="32"/>
<summary>Completion Row Count</summary>
<description>The number of completion rows to display to the user.</description>
</key>
</schema>
</schemalist>
@import url("resource:///org/gnome/builder/themes/shared/shared-buildui.css");
@import url("resource:///org/gnome/builder/themes/shared/shared-completion.css");
@import url("resource:///org/gnome/builder/themes/shared/shared-debugger.css");
@import url("resource:///org/gnome/builder/themes/shared/shared-layout.css");
@import url("resource:///org/gnome/builder/themes/shared/shared-editor.css");
......
completionview,
completionwindow,
completionoverlay {
background: transparent;
}
completionview > * {
margin: 5px 6px 6px 6px;
border: 1px solid @borders;
box-shadow: 0px 0px 3px @wm_shadow;
padding: 0px 3px 0px 3px;
/* on light themes, this won't really do much, but for
* dark themes, it gives us some contrast from dark backgrounds
* such as the builder-dark theme.
*/
background-color: shade(@content_view_bg, 1.5);
border-radius: 5px;
}
completionview list {
padding: 3px 0 3px 0;
background-color: transparent;
}
completionview list row:not(:selected) {
background-color: transparent;
}
completionview list row {
border-radius: 3px;
padding: 3px;
}
completionview list image:first-child {
min-width: 16px;
margin-left: 6px;
margin-right: 6px;
}
completionview list label.left {
margin-right: 12px;
opacity: 0.55;
}
completionview list label.right {
margin-left: 12px;
opacity: 0.55;
}
This diff is collapsed.
/* ide-completion-context.h
*
* Copyright © 2018 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/>.
*/
#pragma once
#include <gtk/gtk.h>
#include "ide-version-macros.h"
#include "completion/ide-completion-types.h"
G_BEGIN_DECLS
#define IDE_TYPE_COMPLETION_CONTEXT (ide_completion_context_get_type())
IDE_AVAILABLE_IN_3_30
G_DECLARE_FINAL_TYPE (IdeCompletionContext, ide_completion_context, IDE, COMPLETION_CONTEXT, GObject)
IDE_AVAILABLE_IN_3_30
IdeCompletionActivation ide_completion_context_get_activation (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
const gchar *ide_completion_context_get_language (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
gboolean ide_completion_context_is_language (IdeCompletionContext *self,
const gchar *language);
IDE_AVAILABLE_IN_3_30
GtkTextBuffer *ide_completion_context_get_buffer (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
GtkTextView *ide_completion_context_get_view (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
gboolean ide_completion_context_get_busy (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
gboolean ide_completion_context_is_empty (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
void ide_completion_context_set_proposals_for_provider (IdeCompletionContext *self,
IdeCompletionProvider *provider,
GListModel *results);
IDE_AVAILABLE_IN_3_30
IdeCompletion *ide_completion_context_get_completion (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
gboolean ide_completion_context_get_bounds (IdeCompletionContext *self,
GtkTextIter *begin,
GtkTextIter *end);
IDE_AVAILABLE_IN_3_30
gboolean ide_completion_context_get_start_iter (IdeCompletionContext *self,
GtkTextIter *iter);
IDE_AVAILABLE_IN_3_30
gchar *ide_completion_context_get_word (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
gchar *ide_completion_context_get_line_text (IdeCompletionContext *self);
IDE_AVAILABLE_IN_3_30
gboolean ide_completion_context_get_item_full (IdeCompletionContext *self,
guint position,
IdeCompletionProvider **provider,
IdeCompletionProposal **proposal);
G_END_DECLS
/* ide-completion-display.c
*
* Copyright 2018 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/>.
*/
#include "config.h"
#define G_LOG_DOMAIN "ide-completion-display"
#include "completion/ide-completion-context.h"
#include "completion/ide-completion-display.h"
#include "sourceview/ide-source-view.h"
G_DEFINE_INTERFACE (IdeCompletionDisplay, ide_completion_display, GTK_TYPE_WIDGET)
static void
ide_completion_display_default_init (IdeCompletionDisplayInterface *iface)
{
}
void
ide_completion_display_set_context (IdeCompletionDisplay *self,
IdeCompletionContext *context)
{
g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
g_return_if_fail (!context || IDE_IS_COMPLETION_CONTEXT (context));
IDE_COMPLETION_DISPLAY_GET_IFACE (self)->set_context (self, context);
}
gboolean
ide_completion_display_key_press_event (IdeCompletionDisplay *self,
const GdkEventKey *key)
{
g_return_val_if_fail (IDE_IS_COMPLETION_DISPLAY (self), FALSE);
g_return_val_if_fail (key!= NULL, FALSE);
return IDE_COMPLETION_DISPLAY_GET_IFACE (self)->key_press_event (self, key);
}
void
ide_completion_display_set_n_rows (IdeCompletionDisplay *self,
guint n_rows)
{
g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
g_return_if_fail (n_rows > 0);
g_return_if_fail (n_rows <= 32);
IDE_COMPLETION_DISPLAY_GET_IFACE (self)->set_n_rows (self, n_rows);
}
void
ide_completion_display_attach (IdeCompletionDisplay *self,
GtkSourceView *view)
{
g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
g_return_if_fail (IDE_IS_SOURCE_VIEW (view));
IDE_COMPLETION_DISPLAY_GET_IFACE (self)->attach (self, view);
}
void
ide_completion_display_move_cursor (IdeCompletionDisplay *self,
GtkMovementStep step,
gint count)
{
g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
IDE_COMPLETION_DISPLAY_GET_IFACE (self)->move_cursor (self, step, count);
}
void
_ide_completion_display_set_font_desc (IdeCompletionDisplay *self,
const PangoFontDescription *font_desc)
{
g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
if (IDE_COMPLETION_DISPLAY_GET_IFACE (self)->set_font_desc)
IDE_COMPLETION_DISPLAY_GET_IFACE (self)->set_font_desc (self, font_desc);
}
/* ide-completion-display.h
*
* Copyright 2018 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/>.
*/
#pragma once
#include <gtksourceview/gtksource.h>
#include "ide-version-macros.h"
#include "completion/ide-completion-types.h"
G_BEGIN_DECLS
#define IDE_TYPE_COMPLETION_DISPLAY (ide_completion_display_get_type())
IDE_AVAILABLE_IN_3_30
G_DECLARE_INTERFACE (IdeCompletionDisplay, ide_completion_display, IDE, COMPLETION_DISPLAY, GtkWidget)
struct _IdeCompletionDisplayInterface
{
GTypeInterface parent_iface;
void (*set_context) (IdeCompletionDisplay *self,
IdeCompletionContext *context);
gboolean (*key_press_event) (IdeCompletionDisplay *self,
const GdkEventKey *key);
void (*attach) (IdeCompletionDisplay *self,
GtkSourceView *view);
void (*set_font_desc) (IdeCompletionDisplay *self,
const PangoFontDescription *font_desc);
void (*set_n_rows) (IdeCompletionDisplay *self,
guint n_rows);
void (*move_cursor) (IdeCompletionDisplay *self,
GtkMovementStep step,
gint count);
};
IDE_AVAILABLE_IN_3_30
void ide_completion_display_attach (IdeCompletionDisplay *self,
GtkSourceView *view);
IDE_AVAILABLE_IN_3_30
void ide_completion_display_set_context (IdeCompletionDisplay *self,
IdeCompletionContext *context);
IDE_AVAILABLE_IN_3_30
gboolean ide_completion_display_key_press_event (IdeCompletionDisplay *self,
const GdkEventKey *key);
IDE_AVAILABLE_IN_3_30
void ide_completion_display_set_n_rows (IdeCompletionDisplay *self,
guint n_rows);
IDE_AVAILABLE_IN_3_30
void ide_completion_display_move_cursor (IdeCompletionDisplay *self,
GtkMovementStep step,
gint count);
G_END_DECLS
/* ide-completion-item.c
*
* Copyright 2015 Christian Hergert <christian@hergert.me>
*
* 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/>.
*/
#define G_LOG_DOMAIN "ide-completion-item"
#include "config.h"
#include <string.h>
#include "completion/ide-completion-item.h"
G_DEFINE_ABSTRACT_TYPE (IdeCompletionItem, ide_completion_item, G_TYPE_OBJECT)
static gboolean
ide_completion_item_real_match (IdeCompletionItem *self,
const gchar *query,
const gchar *casefold)
{
gboolean ret = FALSE;
g_assert (IDE_IS_COMPLETION_ITEM (self));
g_assert (query != NULL);
g_assert (casefold != NULL);
if (GTK_SOURCE_IS_COMPLETION_PROPOSAL (self))
{
gchar *text;
text = gtk_source_completion_proposal_get_label (GTK_SOURCE_COMPLETION_PROPOSAL (self));
ret = !!strstr (text ?: "", query);
g_free (text);
}
return ret;
}
void
ide_completion_item_set_priority (IdeCompletionItem *self,
guint priority)
{
g_return_if_fail (IDE_IS_COMPLETION_ITEM (self));
self->priority = priority;
}
gboolean
ide_completion_item_match (IdeCompletionItem *self,
const gchar *query,
const gchar *casefold)
{
g_return_val_if_fail (IDE_IS_COMPLETION_ITEM (self), FALSE);
return IDE_COMPLETION_ITEM_GET_CLASS (self)->match (self, query, casefold);
}
static void
ide_completion_item_class_init (IdeCompletionItemClass *klass)
{
klass->match = ide_completion_item_real_match;
}
static void
ide_completion_item_init (IdeCompletionItem *self)
{
self->link.data = self;
}
/**
* ide_completion_item_fuzzy_match:
* @haystack: 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 against a batch of #IdeCompletionItem for the best performance.
*
* 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.
*/
gboolean
ide_completion_item_fuzzy_match (const gchar *haystack,
const gchar *casefold_needle,
guint *priority)
{
gint real_score = 0;
for (; *casefold_needle; casefold_needle = g_utf8_next_char (casefold_needle))
{
gunichar ch = g_utf8_get_char (casefold_needle);
const gchar *tmp;
/*
* 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.
*/
tmp = strchr (haystack, ch);
if (tmp == NULL)
{
tmp = strchr (haystack, g_unichar_toupper (ch));
if (tmp == NULL)
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;
/*
* 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;
}
gchar *
ide_completion_item_fuzzy_highlight (const gchar *str,
const gchar *match)
{
static const gchar *begin = "<b>";
static const gchar *end = "</b>";
GString *ret;
gunichar str_ch;
gunichar match_ch;
gboolean element_open = FALSE;
if (str == NULL || match == NULL)
return g_strdup (str);
ret = g_string_new (NULL);
for (; *str; str = g_utf8_next_char (str))
{
str_ch = g_utf8_get_char (str);
match_ch = g_utf8_get_char (match);
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. */
match = g_utf8_next_char (match);
}
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);
}
/* ide-completion-item.h
*
* Copyright 2015 Christian Hergert <christian@hergert.me>
*
* 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/>.
*/
#pragma once
#include <gtksourceview/gtksource.h>
#include "ide-version-macros.h"
G_BEGIN_DECLS
#define IDE_TYPE_COMPLETION_ITEM (ide_completion_item_get_type())
#define IDE_COMPLETION_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), IDE_TYPE_COMPLETION_ITEM, IdeCompletionItem))
#define IDE_COMPLETION_ITEM_CONST(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), IDE_TYPE_COMPLETION_ITEM, IdeCompletionItem const))
#define IDE_COMPLETION_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), IDE_TYPE_COMPLETION_ITEM, IdeCompletionItemClass))
#define IDE_IS_COMPLETION_ITEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IDE_TYPE_COMPLETION_ITEM))
#define IDE_IS_COMPLETION_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), IDE_TYPE_COMPLETION_ITEM))
#define IDE_COMPLETION_ITEM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), IDE_TYPE_COMPLETION_ITEM, IdeCompletionItemClass))
typedef struct _IdeCompletionItem IdeCompletionItem;
typedef struct _IdeCompletionItemClass IdeCompletionItemClass;
struct _IdeCompletionItem
{
GObject parent_instance;
/*< private >*/
GList link;
guint priority;
};
struct _IdeCompletionItemClass
{
GObjectClass parent_class;
/**
* IdeCompletionItem::match:
*
* This virtual function checks to see if a particular query matches
* the #IdeCompletionItem in question. You can use helper functions
* defined in this module for simple requests like case-insensitive
* fuzzy matching.
*
* The default implementation of this virtual function performs a
* strstr() to match @query exactly in the items label.
*
* Returns: %TRUE if the item matches.
*/
gboolean (*match) (IdeCompletionItem *self,
const gchar *query,
const gchar *casefold);
};
G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeCompletionItem, g_object_unref)
IDE_AVAILABLE_IN_ALL
GType ide_completion_item_get_type (void);
IDE_AVAILABLE_IN_ALL
IdeCompletionItem *ide_completion_item_new (void);
IDE_AVAILABLE_IN_ALL
gboolean ide_completion_item_match (IdeCompletionItem *self,
const gchar *query,
const gchar *casefold);
IDE_AVAILABLE_IN_ALL
void ide_completion_item_set_priority (IdeCompletionItem *self,
guint priority);
IDE_AVAILABLE_IN_ALL
gboolean ide_completion_item_fuzzy_match (const gchar *haystack,
const gchar *casefold_needle,
guint *priority);
IDE_AVAILABLE_IN_ALL
gchar *ide_completion_item_fuzzy_highlight (const gchar *haystack,
const gchar *casefold_query);
G_END_DECLS
This diff is collapsed.
/* ide-completion-list-box-row.h
*
* Copyright 2018 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/>.
*/
#pragma once
#include <gtk/gtk.h>
#include "ide-completion-proposal.h"
#include "ide-version-macros.h"
G_BEGIN_DECLS
#define IDE_TYPE_COMPLETION_LIST_BOX_ROW (ide_completion_list_box_row_get_type())
IDE_AVAILABLE_IN_3_30
G_DECLARE_FINAL_TYPE (IdeCompletionListBoxRow, ide_completion_list_box_row, IDE, COMPLETION_LIST_BOX_ROW, GtkListBoxRow)
IDE_AVAILABLE_IN_3_30
GtkWidget *ide_completion_list_box_row_new (void);
IDE_AVAILABLE_IN_3_30
IdeCompletionProposal *ide_completion_list_box_row_get_proposal (IdeCompletionListBoxRow *self);
IDE_AVAILABLE_IN_3_30
void ide_completion_list_box_row_set_proposal (IdeCompletionListBoxRow *self,
IdeCompletionProposal *proposal);
IDE_AVAILABLE_IN_3_30
void ide_completion_list_box_row_set_icon_name (IdeCompletionListBoxRow *self,
const gchar *icon_name);
IDE_AVAILABLE_IN_3_30
void ide_completion_list_box_row_set_left (IdeCompletionListBoxRow *self,
const gchar *left);
IDE_AVAILABLE_IN_3_30
void ide_completion_list_box_row_set_left_markup (IdeCompletionListBoxRow *self,
const gchar *left_markup);
IDE_AVAILABLE_IN_3_30
void ide_completion_list_box_row_set_right (IdeCompletionListBoxRow *self,
const gchar *right);
IDE_AVAILABLE_IN_3_30
void ide_completion_list_box_row_set_center (IdeCompletionListBoxRow *self,
const gchar *center);
IDE_AVAILABLE_IN_3_30
void ide_completion_list_box_row_set_center_markup (IdeCompletionListBoxRow *self,
const gchar *center_markup);
G_END_DECLS
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="IdeCompletionListBoxRow" parent="GtkListBoxRow">
<property name="can-focus">false</property>
<child>
<object class="GtkBox" id="box">
<property name="can-focus">false</property>
<property name="orientation">horizontal</property>
<property name="visible">true</property>
<child>
<object class="GtkImage" id="image">
<property name="valign">center</property>
<property name="visible">true</property>
</object>
</child>
<child>
<object class="GtkLabel" id="left">
<property name="can-focus">false</property>
<property name="xalign">1.0</property>
<property name="hexpand">false</property>
<property name="single-line-mode">true</property>
<property name="visible">true</property>
<style>
<class name="left"/>
</style>
<attributes>
<attribute name="family" value="monospace"/>
</attributes>
</object>
</child>
<child>
<object class="GtkLabel" id="center">
<property name="can-focus">false</property>
<property name="xalign">0.0</property>
<property name="hexpand">true</property>
<property name="single-line-mode">true</property>
<property name="visible">true</property>
<attributes>
<attribute name="family" value="monospace"/>
</attributes>
<style>