medit/junk/completion/completion.c
2010-02-22 21:09:02 -08:00

710 lines
16 KiB
C

/*
* completion.c
*
* Copyright (C) 2004-2007 by Yevgen Muntyan <muntyan@math.tamu.edu>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License version 2.1 as published by the Free Software Foundation.
*
* See COPYING file that comes with this distribution.
*/
#include "completion.h"
#include "mooedit/moocompletionsimple.h"
#include "mooedit/mookeyfile.h"
#include "mooutils/mooutils-misc.h"
#include "mooutils/mooutils-fs.h"
#include "mooutils/moopython.h"
#include <gdk/gdkkeysyms.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <time.h>
typedef enum {
DATA_FILE_SIMPLE,
DATA_FILE_CONFIG,
DATA_FILE_PYTHON
} DataFileType;
#define DATA_FILE_INVALID ((DataFileType) -1)
typedef struct {
char *path;
DataFileType type;
time_t mtime;
MooTextCompletion *cmpl;
} CmplData;
static CmplData *cmpl_data_new (void);
static void cmpl_data_free (CmplData *data);
static void cmpl_data_clear (CmplData *data);
static CmplData *cmpl_plugin_load_data (CmplPlugin *plugin,
const char *id);
static GList *
parse_words (const char *string,
const char *prefix,
const char *path)
{
GList *list = NULL;
char **words, **p;
g_return_val_if_fail (string != NULL, NULL);
words = g_strsplit_set (string, " \t\r\n", 0);
for (p = words; p && *p; ++p)
{
if (p[0][0] && p[0][1])
{
if (!g_utf8_validate (*p, -1, NULL))
{
g_critical ("%s: invalid utf8 in '%s'",
G_STRLOC, path);
}
else if (prefix)
{
list = g_list_prepend (list, g_strdup_printf ("%s%s", prefix, *p));
}
else
{
list = g_list_prepend (list, *p);
*p = NULL;
}
}
g_free (*p);
}
g_free (words);
return g_list_reverse (list);
}
static void
cmpl_data_read_simple_file (CmplData *data)
{
GError *error = NULL;
GList *list;
char *contents;
g_return_if_fail (data->cmpl == NULL);
g_return_if_fail (data->path != NULL);
g_file_get_contents (data->path, &contents, NULL, &error);
if (error)
{
g_warning ("%s: could not read file '%s': %s",
G_STRLOC, data->path, error->message);
g_error_free (error);
return;
}
list = parse_words (contents, NULL, data->path);
data->cmpl = moo_completion_simple_new_text (list);
_moo_message ("read %d words from %s", g_list_length (list), data->path);
g_free (contents);
}
static guint *
parse_numbers (const char *string,
guint *n_numbers_p)
{
guint *numbers;
guint n_numbers, i;
char **pieces, **p;
GSList *list = NULL;
g_return_val_if_fail (string != NULL, NULL);
pieces = g_strsplit_set (string, " \t,;:", 0);
g_return_val_if_fail (pieces != NULL, NULL);
for (p = pieces; p && *p; ++p)
{
if (**p)
{
guint64 n64;
guint n;
errno = 0;
n64 = g_ascii_strtoull (*p, NULL, 10);
if (errno || n64 > 10000)
{
g_warning ("%s: could not parse number '%s'", G_STRLOC, *p);
goto error;
}
n = n64;
list = g_slist_prepend (list, GUINT_TO_POINTER (n));
}
}
list = g_slist_reverse (list);
n_numbers = g_slist_length (list);
numbers = g_new (guint, n_numbers);
for (i = 0; i < n_numbers; ++i)
{
numbers[i] = GPOINTER_TO_UINT (list->data);
list = g_slist_delete_link (list, list);
}
if (n_numbers_p)
*n_numbers_p = n_numbers;
g_strfreev (pieces);
return numbers;
error:
g_strfreev (pieces);
g_slist_free (list);
return NULL;
}
static void
cmpl_data_read_config_file (CmplData *data)
{
MooKeyFile *key_file;
guint i, n_items;
MooCompletionGroup *group = NULL;
GError *error = NULL;
g_return_if_fail (data->cmpl == NULL);
g_return_if_fail (data->path != NULL);
data->cmpl = moo_completion_simple_new_text (NULL);
key_file = moo_key_file_new_from_file (data->path, &error);
if (!key_file)
{
g_warning ("%s", error->message);
g_error_free (error);
return;
}
n_items = moo_key_file_n_items (key_file);
g_return_if_fail (n_items != 0);
for (i = 0; i < n_items; ++i)
{
MooKeyFileItem *item;
const char *pattern, *prefix, *suffix, *script;
const char *groups;
guint *parens;
guint n_parens;
GList *words;
item = moo_key_file_nth_item (key_file, i);
pattern = moo_key_file_item_get (item, "pattern");
prefix = moo_key_file_item_get (item, "prefix");
suffix = moo_key_file_item_get (item, "insert-suffix");
script = moo_key_file_item_get (item, "insert-script");
groups = moo_key_file_item_get (item, "group");
groups = groups ? groups : moo_key_file_item_get (item, "groups");
groups = groups ? groups : "0";
if (!pattern)
{
g_warning ("%s: pattern missing", G_STRLOC);
continue;
}
parens = parse_numbers (groups, &n_parens);
if (!parens)
{
g_warning ("%s: invalid group string '%s'", G_STRLOC, groups);
continue;
}
words = parse_words (moo_key_file_item_get_content (item),
prefix, data->path);
if (!words)
{
g_warning ("%s: empty group", G_STRLOC);
g_free (parens);
continue;
}
_moo_message ("read %d words for patttern '%s' from %s",
g_list_length (words), pattern, data->path);
group = moo_completion_simple_new_group (MOO_COMPLETION_SIMPLE (data->cmpl), NULL);
moo_completion_group_add_data (group, words);
moo_completion_group_set_pattern (group, pattern, parens, n_parens);
if (script && script[0])
moo_completion_group_set_script (group, script);
else if (suffix && suffix[0])
moo_completion_group_set_suffix (group, suffix);
g_free (parens);
}
moo_key_file_unref (key_file);
if (!group)
{
g_warning ("%s: no completions", G_STRLOC);
return;
}
}
static void
cmpl_data_read_python_file (CmplData *data)
{
FILE *file;
MooPyObject *dict = NULL;
MooPyObject *res = NULL;
MooPyObject *py_cmpl = NULL;
MooCompletionSimple *cmpl;
g_return_if_fail (data->cmpl == NULL);
g_return_if_fail (data->path != NULL);
if (!moo_python_running ())
return;
file = _moo_fopen (data->path, "rb");
g_return_if_fail (file != NULL);
dict = moo_python_create_script_dict ("__completion_module__");
res = moo_python_run_file (file, data->path, dict, dict);
fclose (file);
if (!res)
{
g_message ("error executing file '%s'", data->path);
moo_PyErr_Print ();
goto out;
}
py_cmpl = moo_py_dict_get_item (dict, "__completion__");
if (!py_cmpl)
{
g_message ("no '__completion__' variable in file '%s'", data->path);
goto out;
}
cmpl = moo_gobject_from_py_object (py_cmpl);
if (!MOO_IS_TEXT_COMPLETION (cmpl))
{
g_message ("'__completion__' variable in file '%s' is not of type MooTextCompletion", data->path);
goto out;
}
data->cmpl = g_object_ref (cmpl);
out:
if (!data->cmpl)
data->cmpl = moo_completion_simple_new_text (NULL);
moo_Py_DECREF (dict);
moo_Py_DECREF (res);
moo_Py_DECREF (py_cmpl);
}
static void
cmpl_data_read_file (CmplData *data)
{
g_return_if_fail (data->path != NULL);
g_return_if_fail (data->cmpl == NULL);
switch (data->type)
{
case DATA_FILE_SIMPLE:
cmpl_data_read_simple_file (data);
break;
case DATA_FILE_CONFIG:
cmpl_data_read_config_file (data);
break;
case DATA_FILE_PYTHON:
cmpl_data_read_python_file (data);
break;
}
}
static CmplData *
cmpl_plugin_load_data (CmplPlugin *plugin,
const char *id)
{
CmplData *data;
g_return_val_if_fail (id != NULL, NULL);
data = g_hash_table_lookup (plugin->data, id);
if (!data)
{
data = cmpl_data_new ();
g_hash_table_insert (plugin->data, g_strdup (id), data);
}
else if (!data->cmpl && data->path)
{
cmpl_data_read_file (data);
}
return data;
}
static CmplData *
cmpl_data_new (void)
{
return g_new0 (CmplData, 1);
}
static void
cmpl_data_clear (CmplData *data)
{
g_free (data->path);
if (data->cmpl)
g_object_unref (data->cmpl);
data->path = NULL;
data->cmpl = NULL;
}
static void
cmpl_data_free (CmplData *data)
{
if (data)
{
cmpl_data_clear (data);
g_free (data);
}
}
void
_cmpl_plugin_set_lang_completion (CmplPlugin *plugin,
const char *lang,
MooTextCompletion *cmpl)
{
CmplData *data;
g_return_if_fail (!cmpl || MOO_IS_TEXT_COMPLETION (cmpl));
if (!lang || !lang[0])
lang = _moo_lang_id (NULL);
data = cmpl_data_new ();
data->cmpl = g_object_ref (cmpl);
g_hash_table_insert (plugin->data, g_strdup (lang), data);
}
void
_cmpl_plugin_set_doc_completion (CmplPlugin *plugin,
MooEdit *doc,
MooTextCompletion *cmpl)
{
g_return_if_fail (MOO_IS_EDIT (doc));
g_return_if_fail (!cmpl || MOO_IS_TEXT_COMPLETION (cmpl));
g_object_set_qdata_full (G_OBJECT (doc),
plugin->cmpl_quark,
g_object_ref (cmpl),
g_object_unref);
}
static void
cmpl_plugin_check_file (CmplPlugin *plugin,
const char *path,
DataFileType type,
const char *id)
{
struct stat buf;
CmplData *data;
/* TODO: filename encoding */
if (stat (path, &buf) == -1)
{
int err_no = errno;
g_warning ("%s: could not stat file '%s': %s",
G_STRLOC, path, g_strerror (err_no));
return;
}
if (!S_ISREG (buf.st_mode))
{
g_warning ("%s: '%s' is not a regular file",
G_STRLOC, path);
return;
}
data = g_hash_table_lookup (plugin->data, id);
if (data)
{
if (data->path && !strcmp (data->path, path) && data->mtime == buf.st_mtime)
return;
cmpl_data_clear (data);
}
else
{
data = cmpl_data_new ();
g_hash_table_insert (plugin->data, g_strdup (id), data);
}
data->path = g_strdup (path);
data->type = type;
data->mtime = buf.st_mtime;
_moo_message ("found file '%s' for lang '%s'", path, id);
}
static void
cmpl_plugin_load_dir (CmplPlugin *plugin,
const char *path)
{
GDir *dir;
const char *base;
if (!(dir = g_dir_open (path, 0, NULL)))
return;
while ((base = g_dir_read_name (dir)))
{
guint i, len;
char *file, *name, *id;
DataFileType type = DATA_FILE_INVALID;
const char *suffixes[] = {
CMPL_FILE_SUFFIX_LIST,
CMPL_FILE_SUFFIX_CONFIG,
CMPL_FILE_SUFFIX_PYTHON
};
DataFileType types[] = {
DATA_FILE_SIMPLE,
DATA_FILE_CONFIG,
DATA_FILE_PYTHON
};
name = g_ascii_strdown (base, -1);
len = strlen (name);
for (i = 0; i < G_N_ELEMENTS (suffixes); ++i)
{
if (g_str_has_suffix (base, suffixes[i]))
{
name[len - strlen (suffixes[i])] = 0;
if (name[0])
type = types[i];
break;
}
}
if (type == DATA_FILE_INVALID)
continue;
id = _moo_lang_id_from_name (name);
file = g_build_filename (path, base, NULL);
cmpl_plugin_check_file (plugin, file, type, id);
g_free (name);
g_free (id);
g_free (file);
}
g_dir_close (dir);
}
void
_cmpl_plugin_load (CmplPlugin *plugin)
{
char **dirs;
guint n_dirs, i;
plugin->data = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
(GDestroyNotify) cmpl_data_free);
dirs = _moo_strv_reverse (moo_get_data_subdirs (CMPL_DIR, MOO_DATA_SHARE, &n_dirs));
for (i = 0; i < n_dirs; ++i)
cmpl_plugin_load_dir (plugin, dirs[i]);
g_strfreev (dirs);
}
void
_cmpl_plugin_clear (CmplPlugin *plugin)
{
g_hash_table_destroy (plugin->data);
plugin->data = NULL;
}
static MooTextCompletion *
get_doc_completion (CmplPlugin *plugin,
MooEdit *doc)
{
MooTextCompletion *cmpl = NULL;
cmpl = g_object_get_qdata (G_OBJECT (doc), plugin->cmpl_quark);
if (!cmpl)
{
MooLang *lang;
CmplData *data;
lang = moo_text_view_get_lang (MOO_TEXT_VIEW (doc));
data = cmpl_plugin_load_data (plugin, _moo_lang_id (lang));
g_return_val_if_fail (data != NULL, NULL);
cmpl = data->cmpl;
}
return cmpl;
}
static void
completion_finished (MooTextCompletion *cmpl,
CmplPlugin *plugin)
{
plugin->working = FALSE;
g_signal_handlers_disconnect_by_func (cmpl, (gpointer) completion_finished, plugin);
}
void
_completion_complete (CmplPlugin *plugin,
MooEdit *doc,
gboolean automatic)
{
MooTextCompletion *cmpl;
g_return_if_fail (!plugin->working);
g_return_if_fail (MOO_IS_EDIT (doc));
cmpl = get_doc_completion (plugin, doc);
if (cmpl)
{
plugin->working = TRUE;
moo_text_completion_set_doc (cmpl, GTK_TEXT_VIEW (doc));
g_signal_connect (cmpl, "finish", G_CALLBACK (completion_finished), plugin);
moo_text_completion_try_complete (cmpl, automatic);
}
}
static gboolean
popup_timeout (CmplPlugin *plugin)
{
plugin->popup_timeout = 0;
if (!plugin->working)
_completion_complete (plugin, plugin->focused_doc, TRUE);
return FALSE;
}
static void
remove_popup_timeout (CmplPlugin *plugin)
{
if (plugin->popup_timeout)
g_source_remove (plugin->popup_timeout);
plugin->popup_timeout = 0;
}
static void
reinstall_popup_timeout (CmplPlugin *plugin)
{
remove_popup_timeout (plugin);
if (!plugin->working)
{
if (!plugin->popup_timeout)
plugin->popup_timeout =
_moo_timeout_add (plugin->popup_interval, (GSourceFunc) popup_timeout, plugin);
}
}
static gboolean
doc_key_press (CmplPlugin *plugin)
{
remove_popup_timeout (plugin);
return FALSE;
}
static gboolean
doc_char_inserted (CmplPlugin *plugin)
{
reinstall_popup_timeout (plugin);
return FALSE;
}
static void
connect_doc (CmplPlugin *plugin,
MooEdit *doc)
{
g_signal_connect_swapped (doc, "key-press-event",
G_CALLBACK (doc_key_press),
plugin);
g_signal_connect_swapped (doc, "char-inserted",
G_CALLBACK (doc_char_inserted),
plugin);
}
static void
disconnect_doc (CmplPlugin *plugin,
MooEdit *doc)
{
remove_popup_timeout (plugin);
g_signal_handlers_disconnect_by_func (doc, (gpointer) doc_key_press, plugin);
g_signal_handlers_disconnect_by_func (doc, (gpointer) doc_char_inserted, plugin);
}
void
_cmpl_plugin_set_focused_doc (CmplPlugin *plugin,
MooEdit *doc)
{
if (plugin->focused_doc == doc)
return;
if (plugin->focused_doc)
{
disconnect_doc (plugin, plugin->focused_doc);
plugin->focused_doc = NULL;
}
if (doc)
{
plugin->focused_doc = doc;
connect_doc (plugin, doc);
}
}