gem-graph-client/libide/sourceview/ide-snippet-context.c

776 lines
18 KiB
C

/* ide-snippet-context.c
*
* Copyright 2013-2019 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/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "ide-snippets-context"
#include "config.h"
#include <errno.h>
#include <glib/gi18n.h>
#include <stdlib.h>
#include "ide-snippet-context.h"
/**
* SECTION:ide-snippet-context
* @title: IdeSnippetContext
* @short_description: Context for expanding #IdeSnippetChunk
*
* This class is currently used primary as a hashtable. However, the longer
* term goal is to have it hold onto a GjsContext as well as other languages
* so that #IdeSnippetChunk can expand themselves by executing
* script within the context.
*
* The #IdeSnippet will build the context and then expand each of the
* chunks during the insertion/edit phase.
*
* Since: 3.32
*/
struct _IdeSnippetContext
{
GObject parent_instance;
GHashTable *shared;
GHashTable *variables;
gchar *line_prefix;
gint tab_width;
guint use_spaces : 1;
};
struct _IdeSnippetContextClass
{
GObjectClass parent;
};
G_DEFINE_FINAL_TYPE (IdeSnippetContext, ide_snippet_context, G_TYPE_OBJECT)
enum {
CHANGED,
LAST_SIGNAL
};
typedef gchar *(*InputFilter) (const gchar *input);
static GHashTable *filters;
static guint signals[LAST_SIGNAL];
IdeSnippetContext *
ide_snippet_context_new (void)
{
return g_object_new (IDE_TYPE_SNIPPET_CONTEXT, NULL);
}
void
ide_snippet_context_dump (IdeSnippetContext *context)
{
GHashTableIter iter;
gpointer key;
gpointer value;
g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
g_hash_table_iter_init (&iter, context->variables);
while (g_hash_table_iter_next (&iter, &key, &value))
g_print (" %s=%s\n", (gchar *) key, (gchar *) value);
}
void
ide_snippet_context_clear_variables (IdeSnippetContext *context)
{
g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
g_hash_table_remove_all (context->variables);
}
void
ide_snippet_context_add_variable (IdeSnippetContext *context,
const gchar *key,
const gchar *value)
{
g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
g_return_if_fail (key);
g_hash_table_replace (context->variables, g_strdup (key), g_strdup (value));
}
void
ide_snippet_context_add_shared_variable (IdeSnippetContext *context,
const gchar *key,
const gchar *value)
{
g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
g_return_if_fail (key);
g_hash_table_replace (context->shared, g_strdup (key), g_strdup (value));
}
const gchar *
ide_snippet_context_get_variable (IdeSnippetContext *context,
const gchar *key)
{
const gchar *ret;
g_return_val_if_fail (IDE_IS_SNIPPET_CONTEXT (context), NULL);
if (!(ret = g_hash_table_lookup (context->variables, key)))
ret = g_hash_table_lookup (context->shared, key);
return ret;
}
static gchar *
filter_lower (const gchar *input)
{
return g_utf8_strdown (input, -1);
}
static gchar *
filter_upper (const gchar *input)
{
return g_utf8_strup (input, -1);
}
static gchar *
filter_capitalize (const gchar *input)
{
gunichar c;
GString *str;
if (!*input)
return g_strdup ("");
c = g_utf8_get_char (input);
if (g_unichar_isupper (c))
return g_strdup (input);
str = g_string_new (NULL);
input = g_utf8_next_char (input);
g_string_append_unichar (str, g_unichar_toupper (c));
if (*input)
g_string_append (str, input);
return g_string_free (str, FALSE);
}
static gchar *
filter_decapitalize (const gchar *input)
{
gunichar c;
GString *str;
c = g_utf8_get_char (input);
if (g_unichar_islower (c))
return g_strdup (input);
str = g_string_new (NULL);
input = g_utf8_next_char (input);
g_string_append_unichar (str, g_unichar_tolower (c));
g_string_append (str, input);
return g_string_free (str, FALSE);
}
static gchar *
filter_html (const gchar *input)
{
gunichar c;
GString *str;
str = g_string_new (NULL);
for (; *input; input = g_utf8_next_char (input))
{
c = g_utf8_get_char (input);
switch (c)
{
case '<':
g_string_append_len (str, "&lt;", 4);
break;
case '>':
g_string_append_len (str, "&gt;", 4);
break;
default:
g_string_append_unichar (str, c);
break;
}
}
return g_string_free (str, FALSE);
}
static gchar *
filter_camelize (const gchar *input)
{
gboolean next_is_upper = TRUE;
gboolean skip = FALSE;
gunichar c;
GString *str;
if (!strchr (input, '_') && !strchr (input, ' ') && !strchr (input, '-'))
return filter_capitalize (input);
str = g_string_new (NULL);
for (; *input; input = g_utf8_next_char (input))
{
c = g_utf8_get_char (input);
switch (c)
{
case '_':
case '-':
case ' ':
next_is_upper = TRUE;
skip = TRUE;
break;
default:
break;
}
if (skip)
{
skip = FALSE;
continue;
}
if (next_is_upper)
{
c = g_unichar_toupper (c);
next_is_upper = FALSE;
}
else
c = g_unichar_tolower (c);
g_string_append_unichar (str, c);
}
if (g_str_has_suffix (str->str, "Private"))
g_string_truncate (str, str->len - strlen ("Private"));
return g_string_free (str, FALSE);
}
static gchar *
filter_functify (const gchar *input)
{
gunichar last = 0;
gunichar c;
gunichar n;
GString *str;
str = g_string_new (NULL);
for (; *input; input = g_utf8_next_char (input))
{
c = g_utf8_get_char (input);
n = g_utf8_get_char (g_utf8_next_char (input));
if (last)
{
if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
(g_unichar_isupper (c) && g_unichar_islower (n)))
g_string_append_c (str, '_');
}
if ((c == ' ') || (c == '-'))
c = '_';
g_string_append_unichar (str, g_unichar_tolower (c));
last = c;
}
if (g_str_has_suffix (str->str, "_private") ||
g_str_has_suffix (str->str, "_PRIVATE"))
g_string_truncate (str, str->len - strlen ("_private"));
return g_string_free (str, FALSE);
}
static gchar *
filter_namespace (const gchar *input)
{
gunichar last = 0;
gunichar c;
gunichar n;
GString *str;
gboolean first_is_lower = FALSE;
str = g_string_new (NULL);
for (; *input; input = g_utf8_next_char (input))
{
c = g_utf8_get_char (input);
n = g_utf8_get_char (g_utf8_next_char (input));
if (c == '_')
break;
if (last)
{
if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
(g_unichar_isupper (c) && g_unichar_islower (n)))
break;
}
else
first_is_lower = g_unichar_islower (c);
if ((c == ' ') || (c == '-'))
break;
g_string_append_unichar (str, c);
last = c;
}
if (first_is_lower)
{
gchar *ret;
ret = filter_capitalize (str->str);
g_string_free (str, TRUE);
return ret;
}
return g_string_free (str, FALSE);
}
static gchar *
filter_class (const gchar *input)
{
gchar *camel;
gchar *ns;
gchar *ret = NULL;
camel = filter_camelize (input);
ns = filter_namespace (input);
if (g_str_has_prefix (camel, ns))
ret = g_strdup (camel + strlen (ns));
else
{
ret = camel;
camel = NULL;
}
g_free (camel);
g_free (ns);
return ret;
}
static gchar *
filter_instance (const gchar *input)
{
const gchar *tmp;
gchar *funct = NULL;
gchar *ret;
if (!strchr (input, '_'))
{
funct = filter_functify (input);
input = funct;
}
if ((tmp = strrchr (input, '_')))
ret = g_strdup (tmp+1);
else
ret = g_strdup (input);
g_free (funct);
return ret;
}
static gchar *
filter_space (const gchar *input)
{
GString *str;
str = g_string_new (NULL);
for (; *input; input = g_utf8_next_char (input))
g_string_append_c (str, ' ');
return g_string_free (str, FALSE);
}
static gchar *
filter_descend_path (const gchar *input)
{
const gchar *pos;
if (input == NULL)
return NULL;
while (*input == G_DIR_SEPARATOR)
input++;
if ((pos = strchr (input, G_DIR_SEPARATOR)))
return g_strdup (pos + 1);
return NULL;
}
static gchar *
filter_stripsuffix (const gchar *input)
{
const gchar *endpos;
g_return_val_if_fail (input, NULL);
endpos = strrchr (input, '.');
if (endpos)
return g_strndup (input, (endpos - input));
return g_strdup (input);
}
static gchar *
filter_slash_to_dots (const gchar *input)
{
GString *str;
gunichar ch;
if (input == NULL)
return NULL;
str = g_string_new (NULL);
for (; *input; input = g_utf8_next_char (input))
{
ch = g_utf8_get_char (input);
if (ch == G_DIR_SEPARATOR)
g_string_append_c (str, '.');
else
g_string_append_unichar (str, ch);
}
return g_string_free (str, FALSE);
}
static gchar *
apply_filter (gchar *input,
const gchar *filter)
{
InputFilter filter_func;
gchar *tmp;
if ((filter_func = g_hash_table_lookup (filters, filter)))
{
tmp = input;
input = filter_func (input);
g_free (tmp);
}
return input;
}
static gchar *
apply_filters (GString *str,
const gchar *filters_list)
{
gchar **filter_names;
gchar *input = g_string_free (str, FALSE);
gint i;
filter_names = g_strsplit (filters_list, "|", 0);
for (i = 0; filter_names[i]; i++)
input = apply_filter (input, filter_names[i]);
g_strfreev (filter_names);
return input;
}
static gchar *
scan_forward (const gchar *input,
const gchar **endpos,
gunichar needle)
{
const gchar *begin = input;
for (; *input; input = g_utf8_next_char (input))
{
gunichar c = g_utf8_get_char (input);
if (c == needle)
{
*endpos = input;
return g_strndup (begin, (input - begin));
}
}
*endpos = NULL;
return NULL;
}
gchar *
ide_snippet_context_expand (IdeSnippetContext *context,
const gchar *input)
{
const gchar *expand;
gunichar c;
gboolean is_dynamic;
GString *str;
gchar key[12];
glong n;
gint i;
g_return_val_if_fail (IDE_IS_SNIPPET_CONTEXT (context), NULL);
g_return_val_if_fail (input, NULL);
is_dynamic = (*input == '$');
str = g_string_new (NULL);
for (; *input; input = g_utf8_next_char (input))
{
c = g_utf8_get_char (input);
if (c == '\\')
{
input = g_utf8_next_char (input);
if (!*input)
break;
c = g_utf8_get_char (input);
}
else if (is_dynamic && c == '$')
{
input = g_utf8_next_char (input);
if (!*input)
break;
c = g_utf8_get_char (input);
if (g_unichar_isdigit (c))
{
errno = 0;
n = strtol (input, (gchar * *) &input, 10);
if (((n == LONG_MIN) || (n == LONG_MAX)) && errno == ERANGE)
break;
input--;
g_snprintf (key, sizeof key, "%ld", n);
key[sizeof key - 1] = '\0';
expand = ide_snippet_context_get_variable (context, key);
if (expand)
g_string_append (str, expand);
continue;
}
else
{
if (strchr (input, '|'))
{
g_autofree gchar *lkey = NULL;
lkey = g_strndup (input, strchr (input, '|') - input);
expand = ide_snippet_context_get_variable (context, lkey);
if (expand)
{
g_string_append (str, expand);
input = strchr (input, '|') - 1;
}
else
input += strlen (input) - 1;
}
else
{
expand = ide_snippet_context_get_variable (context, input);
if (expand)
g_string_append (str, expand);
else
{
g_string_append_c (str, '$');
g_string_append (str, input);
}
input += strlen (input) - 1;
}
continue;
}
}
else if (is_dynamic && c == '|')
return apply_filters (str, input + 1);
else if (c == '`')
{
const gchar *endpos = NULL;
gchar *slice;
slice = scan_forward (input + 1, &endpos, '`');
if (slice)
{
gchar *expanded;
input = endpos;
expanded = ide_snippet_context_expand (context, slice);
g_string_append (str, expanded);
g_free (expanded);
g_free (slice);
continue;
}
}
else if (c == '\t')
{
if (context->use_spaces)
for (i = 0; i < context->tab_width; i++)
g_string_append_c (str, ' ');
else
g_string_append_c (str, '\t');
continue;
}
else if (c == '\n')
{
g_string_append_c (str, '\n');
if (context->line_prefix)
g_string_append (str, context->line_prefix);
continue;
}
g_string_append_unichar (str, c);
}
return g_string_free (str, FALSE);
}
void
ide_snippet_context_set_tab_width (IdeSnippetContext *context,
gint tab_width)
{
g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
context->tab_width = tab_width;
}
void
ide_snippet_context_set_use_spaces (IdeSnippetContext *context,
gboolean use_spaces)
{
g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
context->use_spaces = !!use_spaces;
}
void
ide_snippet_context_set_line_prefix (IdeSnippetContext *context,
const gchar *line_prefix)
{
g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
g_free (context->line_prefix);
context->line_prefix = g_strdup (line_prefix);
}
void
ide_snippet_context_emit_changed (IdeSnippetContext *context)
{
g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
g_signal_emit (context, signals[CHANGED], 0);
}
static void
ide_snippet_context_finalize (GObject *object)
{
IdeSnippetContext *context = (IdeSnippetContext *)object;
g_clear_pointer (&context->shared, g_hash_table_unref);
g_clear_pointer (&context->variables, g_hash_table_unref);
g_clear_pointer (&context->line_prefix, g_free);
G_OBJECT_CLASS (ide_snippet_context_parent_class)->finalize (object);
}
static void
ide_snippet_context_class_init (IdeSnippetContextClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = ide_snippet_context_finalize;
signals[CHANGED] = g_signal_new ("changed",
IDE_TYPE_SNIPPET_CONTEXT,
G_SIGNAL_RUN_FIRST,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
0);
filters = g_hash_table_new (g_str_hash, g_str_equal);
g_hash_table_insert (filters, (gpointer) "lower", filter_lower);
g_hash_table_insert (filters, (gpointer) "upper", filter_upper);
g_hash_table_insert (filters, (gpointer) "capitalize", filter_capitalize);
g_hash_table_insert (filters, (gpointer) "decapitalize", filter_decapitalize);
g_hash_table_insert (filters, (gpointer) "html", filter_html);
g_hash_table_insert (filters, (gpointer) "camelize", filter_camelize);
g_hash_table_insert (filters, (gpointer) "functify", filter_functify);
g_hash_table_insert (filters, (gpointer) "namespace", filter_namespace);
g_hash_table_insert (filters, (gpointer) "class", filter_class);
g_hash_table_insert (filters, (gpointer) "space", filter_space);
g_hash_table_insert (filters, (gpointer) "stripsuffix", filter_stripsuffix);
g_hash_table_insert (filters, (gpointer) "instance", filter_instance);
g_hash_table_insert (filters, (gpointer) "slash_to_dots", filter_slash_to_dots);
g_hash_table_insert (filters, (gpointer) "descend_path", filter_descend_path);
}
static void
ide_snippet_context_init (IdeSnippetContext *context)
{
GDateTime *dt;
gchar *str;
context->variables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
context->shared = g_hash_table_new_full (g_str_hash,
g_str_equal,
g_free,
g_free);
#define ADD_VARIABLE(k, v) \
g_hash_table_insert (context->shared, g_strdup (k), g_strdup (v))
ADD_VARIABLE ("username", g_get_user_name ());
ADD_VARIABLE ("fullname", g_get_real_name ());
ADD_VARIABLE ("author", g_get_real_name ());
dt = g_date_time_new_now_local ();
str = g_date_time_format (dt, "%Y");
ADD_VARIABLE ("year", str);
g_free (str);
str = g_date_time_format (dt, "%b");
ADD_VARIABLE ("shortmonth", str);
g_free (str);
str = g_date_time_format (dt, "%d");
ADD_VARIABLE ("day", str);
g_free (str);
str = g_date_time_format (dt, "%a");
ADD_VARIABLE ("shortweekday", str);
g_free (str);
g_date_time_unref (dt);
ADD_VARIABLE ("email", "unknown@domain.org");
#undef ADD_VARIABLE
}