[gtksourceview] snippets: avoid inflation of snippets



commit 1d35bac1ece131c7b4a4bd4a8ea23744183ebcb3
Author: Christian Hergert <chergert redhat com>
Date:   Fri Jul 22 17:53:16 2022 -0700

    snippets: avoid inflation of snippets
    
    This avoids inflating GtkSourceSnippet until the snippet is activated. That
    saves a bunch of parsing while searching, but also allows us to handle much
    larger snippet sets.
    
    We now use strstr() to find a matchin substring to match.
    
    Additionally, we use GtkCustomFilter which can properly re-use existing
    search state instead of starting from scratch on each keypress.

 .../snippets/gtksourcecompletionsnippets.c         | 210 +++++++++++++--------
 .../gtksourcecompletionsnippetsproposal-private.h  |  13 +-
 .../snippets/gtksourcecompletionsnippetsproposal.c |  82 ++------
 3 files changed, 160 insertions(+), 145 deletions(-)
---
diff --git a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.c 
b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.c
index 1a8cb6c6..afa36c71 100644
--- a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.c
+++ b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippets.c
@@ -45,6 +45,25 @@
  * registered with the [class@SnippetManager].
  */
 
+typedef struct
+{
+       char *word;
+       int minimum_word_size;
+       guint filter_all : 1;
+} FilterData;
+
+static void
+filter_data_finalize (FilterData *fd)
+{
+       g_clear_pointer (&fd->word, g_free);
+}
+
+static void
+filter_data_release (FilterData *fd)
+{
+       g_atomic_rc_box_release_full (fd, (GDestroyNotify)filter_data_finalize);
+}
+
 struct _GtkSourceSnippetResults
 {
        GObject     parent_instance;
@@ -68,7 +87,13 @@ static guint
 gtk_source_snippet_results_get_n_items (GListModel *model)
 {
        GtkSourceSnippetResults *self = (GtkSourceSnippetResults *)model;
-       return g_list_model_get_n_items (self->snippets);
+
+       if (self->snippets != NULL)
+       {
+               return g_list_model_get_n_items (self->snippets);
+       }
+
+       return 0;
 }
 
 static gpointer
@@ -76,8 +101,17 @@ gtk_source_snippet_results_get_item (GListModel *model,
                                      guint       position)
 {
        GtkSourceSnippetResults *self = (GtkSourceSnippetResults *)model;
-       GtkSourceSnippet *snippet = g_list_model_get_item (self->snippets, position);
-       return gtk_source_completion_snippets_proposal_new (snippet);
+
+       g_assert (!self->snippets || GTK_SOURCE_IS_SNIPPET_BUNDLE (self->snippets));
+
+       if (self->snippets != NULL)
+       {
+               GtkSourceSnippetBundle *bundle = GTK_SOURCE_SNIPPET_BUNDLE (self->snippets);
+               const GtkSourceSnippetInfo *info = _gtk_source_snippet_bundle_get_info (bundle, position);
+               return gtk_source_completion_snippets_proposal_new (bundle, info);
+       }
+
+       return NULL;
 }
 
 static void
@@ -92,7 +126,7 @@ static GListModel *
 gtk_source_snippet_results_new (GListModel *base_model)
 {
        GtkSourceSnippetResults *self = g_object_new (GTK_SOURCE_TYPE_SNIPPET_RESULTS, NULL);
-       self->snippets = g_object_ref (base_model);
+       g_set_object (&self->snippets, base_model);
        return G_LIST_MODEL (self);
 }
 
@@ -121,17 +155,16 @@ gtk_source_snippet_results_init (GtkSourceSnippetResults *self)
 
 typedef struct
 {
-       char *title;
-       int   priority;
-       int   minimum_word_size;
+       FilterData *filter_data;
+       char       *title;
+       int         priority;
 } GtkSourceCompletionSnippetsPrivate;
 
 static void completion_provider_iface_init (GtkSourceCompletionProviderInterface *iface);
 
 G_DEFINE_TYPE_WITH_CODE (GtkSourceCompletionSnippets, gtk_source_completion_snippets, G_TYPE_OBJECT,
                          G_ADD_PRIVATE (GtkSourceCompletionSnippets)
-                         G_IMPLEMENT_INTERFACE (GTK_SOURCE_TYPE_COMPLETION_PROVIDER,
-                                                completion_provider_iface_init))
+                         G_IMPLEMENT_INTERFACE (GTK_SOURCE_TYPE_COMPLETION_PROVIDER, 
completion_provider_iface_init))
 
 enum {
        PROP_0,
@@ -142,6 +175,34 @@ enum {
 
 static GParamSpec *properties[N_PROPS];
 
+static gboolean
+filter_snippet_func (gpointer item,
+                    gpointer user_data)
+{
+       GtkSourceCompletionSnippetsProposal *proposal = item;
+       FilterData *fd = user_data;
+
+       g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS_PROPOSAL (proposal));
+       g_assert (fd != NULL);
+
+       if (fd->filter_all)
+       {
+               return FALSE;
+       }
+
+       if (proposal->info.trigger == NULL)
+       {
+               return FALSE;
+       }
+
+       /* We could do fuzzy here, or we could also do case-insensitive (strcasestr),
+        * but generally, having case match can be helpful in it's own. We could always
+        * add more tweaks to this if they become necessary.
+        */
+
+       return strstr (proposal->info.trigger, fd->word) != NULL;
+}
+
 static GListModel *
 gtk_source_completion_snippets_populate (GtkSourceCompletionProvider  *provider,
                                          GtkSourceCompletionContext   *context,
@@ -151,57 +212,51 @@ gtk_source_completion_snippets_populate (GtkSourceCompletionProvider  *provider,
        GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(snippets);
        GtkSourceCompletionActivation activation;
        GtkSourceSnippetManager *manager;
+       GtkFilterListModel *filter_model;
        GtkSourceLanguage *language;
+       GtkCustomFilter *filter;
        GtkSourceBuffer *buffer;
-       const gchar *language_id = "";
-       GtkTextIter begin, end;
+       const char *language_id = "";
        GListModel *matches;
-       GListModel *results = NULL;
-       gchar *word;
+       GListModel *results;
 
-       if (!gtk_source_completion_context_get_bounds (context, &begin, &end))
-       {
-               return NULL;
-       }
+       g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS (snippets));
+       g_assert (GTK_SOURCE_IS_COMPLETION_CONTEXT (context));
 
        buffer = gtk_source_completion_context_get_buffer (context);
-       word = gtk_source_completion_context_get_word (context);
        activation = gtk_source_completion_context_get_activation (context);
        manager = gtk_source_snippet_manager_get_default ();
        language = gtk_source_buffer_get_language (buffer);
 
-       if (word == NULL ||
-           (activation == GTK_SOURCE_COMPLETION_ACTIVATION_INTERACTIVE &&
-            g_utf8_strlen (word, -1) < (glong)priv->minimum_word_size))
-       {
-               g_free (word);
-               return NULL;
-       }
+       /* Update state in indirected struct for filters */
+       g_free (priv->filter_data->word);
+       priv->filter_data->word = gtk_source_completion_context_get_word (context);
+
+       priv->filter_data->filter_all =
+               (priv->filter_data->word == NULL ||
+                (activation == GTK_SOURCE_COMPLETION_ACTIVATION_INTERACTIVE &&
+                 g_utf8_strlen (priv->filter_data->word, -1) < (glong)priv->filter_data->minimum_word_size));
 
        if (language != NULL)
        {
                language_id = gtk_source_language_get_id (language);
        }
 
-       matches = gtk_source_snippet_manager_list_matching (manager, NULL, language_id, word);
-
-       if (matches != NULL)
-       {
-               results = gtk_source_snippet_results_new (matches);
-       }
-
+       matches = gtk_source_snippet_manager_list_matching (manager, NULL, language_id, NULL);
+       results = gtk_source_snippet_results_new (matches);
        g_clear_object (&matches);
-       g_free (word);
 
-       if (results == NULL)
-       {
-               g_set_error (error,
-                            G_IO_ERROR,
-                            G_IO_ERROR_NOT_SUPPORTED,
-                            "No results");
-       }
+       /* Create a filter we can re-use so we get optimal updates on
+        * change (only refilter the portions either matching or not-matching
+        * when words change).
+        */
+       filter = gtk_custom_filter_new (filter_snippet_func,
+                                       g_atomic_rc_box_acquire (priv->filter_data),
+                                       (GDestroyNotify)filter_data_release);
+       filter_model = gtk_filter_list_model_new (results, GTK_FILTER (filter));
+       gtk_filter_list_model_set_incremental (filter_model, TRUE);
 
-       return results;
+       return G_LIST_MODEL (filter_model);
 }
 
 static gchar *
@@ -228,10 +283,11 @@ gtk_source_completion_snippets_activate (GtkSourceCompletionProvider *provider,
                                          GtkSourceCompletionContext  *context,
                                          GtkSourceCompletionProposal *proposal)
 {
+       GtkSourceCompletionSnippets *self = (GtkSourceCompletionSnippets *)provider;
        GtkSourceCompletionSnippetsProposal *p = (GtkSourceCompletionSnippetsProposal *)proposal;
        GtkTextIter begin, end;
 
-       g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS (provider));
+       g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS (self));
        g_assert (GTK_SOURCE_IS_COMPLETION_CONTEXT (context));
        g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS_PROPOSAL (p));
 
@@ -239,17 +295,16 @@ gtk_source_completion_snippets_activate (GtkSourceCompletionProvider *provider,
        {
                GtkTextBuffer *buffer = gtk_text_iter_get_buffer (&begin);
                GtkSourceView *view = gtk_source_completion_context_get_view (context);
-               GtkSourceSnippet *snippet, *copy;
+               GtkSourceSnippet *snippet;
 
-               snippet = gtk_source_completion_snippets_proposal_get_snippet (p);
-               copy = gtk_source_snippet_copy (snippet);
+               snippet = gtk_source_completion_snippets_proposal_dup_snippet (p);
 
                gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
                gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &begin, &end);
-               gtk_source_view_push_snippet (view, copy, &begin);
+               gtk_source_view_push_snippet (view, snippet, &begin);
                gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
 
-               g_object_unref (copy);
+               g_object_unref (snippet);
        }
 }
 
@@ -261,19 +316,17 @@ gtk_source_completion_snippets_display (GtkSourceCompletionProvider *provider,
 {
        GtkSourceCompletionSnippetsProposal *p = (GtkSourceCompletionSnippetsProposal *)proposal;
        GtkSourceCompletionColumn column;
-       GtkSourceSnippet *snippet;
 
        g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS (provider));
        g_assert (GTK_SOURCE_IS_COMPLETION_CONTEXT (context));
        g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS_PROPOSAL (p));
        g_assert (GTK_SOURCE_IS_COMPLETION_CELL (cell));
 
-       snippet = gtk_source_completion_snippets_proposal_get_snippet (p);
        column = gtk_source_completion_cell_get_column (cell);
 
        if (column == GTK_SOURCE_COMPLETION_COLUMN_TYPED_TEXT)
        {
-               const char *trigger = gtk_source_snippet_get_trigger (snippet);
+               const char *trigger = p->info.trigger;
                char *word = gtk_source_completion_context_get_word (context);
                PangoAttrList *highlight = gtk_source_completion_fuzzy_highlight (trigger, word);
 
@@ -284,18 +337,15 @@ gtk_source_completion_snippets_display (GtkSourceCompletionProvider *provider,
        }
        else if (column == GTK_SOURCE_COMPLETION_COLUMN_ICON)
        {
-               gtk_source_completion_cell_set_icon_name (cell,
-                                                         "completion-snippet-symbolic");
+               gtk_source_completion_cell_set_icon_name (cell, "completion-snippet-symbolic");
        }
        else if (column == GTK_SOURCE_COMPLETION_COLUMN_COMMENT)
        {
-               gtk_source_completion_cell_set_text (cell,
-                                                    gtk_source_snippet_get_trigger (snippet));
+               gtk_source_completion_cell_set_text (cell, p->info.trigger);
        }
        else if (column == GTK_SOURCE_COMPLETION_COLUMN_DETAILS)
        {
-               gtk_source_completion_cell_set_text (cell,
-                                                    gtk_source_snippet_get_description (snippet));
+               gtk_source_completion_cell_set_text (cell, p->info.description);
        }
 }
 
@@ -304,39 +354,41 @@ gtk_source_completion_snippets_refilter (GtkSourceCompletionProvider *provider,
                                          GtkSourceCompletionContext  *context,
                                          GListModel                  *model)
 {
-       GtkFilterListModel *filter_model;
-       GtkExpression *expression;
-       GtkStringFilter *filter;
-       gchar *word;
+       GtkSourceCompletionSnippets *self = (GtkSourceCompletionSnippets *)provider;
+       GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private (self);
+       GtkFilterChange change;
+       GtkFilter *filter;
+       char *old_word;
+       char *word;
 
-       g_assert (GTK_SOURCE_IS_COMPLETION_PROVIDER (provider));
+       g_assert (GTK_SOURCE_IS_COMPLETION_SNIPPETS (self));
        g_assert (GTK_SOURCE_IS_COMPLETION_CONTEXT (context));
-       g_assert (G_IS_LIST_MODEL (model));
+       g_assert (GTK_IS_FILTER_LIST_MODEL (model));
 
        word = gtk_source_completion_context_get_word (context);
+       old_word = g_steal_pointer (&priv->filter_data->word);
 
-       if (GTK_IS_FILTER_LIST_MODEL (model))
+       if (old_word && g_str_has_prefix (word, old_word))
+       {
+               change = GTK_FILTER_CHANGE_MORE_STRICT;
+       }
+       else
        {
-               model = gtk_filter_list_model_get_model (GTK_FILTER_LIST_MODEL (model));
+               change = GTK_FILTER_CHANGE_LESS_STRICT;
        }
 
-       if (!word || !word[0])
+       if (priv->filter_data->filter_all)
        {
-               gtk_source_completion_context_set_proposals_for_provider (context, provider, model);
-               g_free (word);
-               return;
+               priv->filter_data->filter_all = FALSE;
+               change = GTK_FILTER_CHANGE_LESS_STRICT;
        }
 
-       expression = gtk_property_expression_new (GTK_SOURCE_TYPE_COMPLETION_SNIPPETS_PROPOSAL, NULL, 
"trigger");
-       filter = gtk_string_filter_new (g_steal_pointer (&expression));
-       gtk_string_filter_set_search (GTK_STRING_FILTER (filter), word);
-       filter_model = gtk_filter_list_model_new (g_object_ref (model),
-                                                 GTK_FILTER (g_steal_pointer (&filter)));
-       gtk_filter_list_model_set_incremental (filter_model, TRUE);
-       gtk_source_completion_context_set_proposals_for_provider (context, provider, G_LIST_MODEL 
(filter_model));
+       priv->filter_data->word = word;
+
+       filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (model));
+       gtk_filter_changed (filter, change);
 
-       g_clear_object (&filter_model);
-       g_free (word);
+       g_free (old_word);
 }
 
 
@@ -358,6 +410,7 @@ gtk_source_completion_snippets_finalize (GObject *object)
        GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private 
(provider);
 
        g_clear_pointer (&priv->title, g_free);
+       g_clear_pointer (&priv->filter_data, filter_data_release);
 
        G_OBJECT_CLASS (gtk_source_completion_snippets_parent_class)->finalize (object);
 }
@@ -451,7 +504,8 @@ gtk_source_completion_snippets_init (GtkSourceCompletionSnippets *self)
 {
        GtkSourceCompletionSnippetsPrivate *priv = gtk_source_completion_snippets_get_instance_private (self);
 
-       priv->minimum_word_size = 2;
+       priv->filter_data = g_atomic_rc_box_alloc0 (sizeof (FilterData));
+       priv->filter_data->minimum_word_size = 2;
 }
 
 GtkSourceCompletionSnippets *
diff --git a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal-private.h 
b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal-private.h
index 3469fbf5..c07735c1 100644
--- a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal-private.h
+++ b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal-private.h
@@ -21,6 +21,7 @@
 
 #include <gtksourceview/gtksourcetypes.h>
 #include <gtksourceview/gtksourcecompletionproposal.h>
+#include <gtksourceview/gtksourcesnippetbundle-private.h>
 
 G_BEGIN_DECLS
 
@@ -28,7 +29,15 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GtkSourceCompletionSnippetsProposal, gtk_source_completion_snippets_proposal, 
GTK_SOURCE, COMPLETION_SNIPPETS_PROPOSAL, GObject)
 
-GtkSourceCompletionProposal *gtk_source_completion_snippets_proposal_new         (GtkSourceSnippet           
         *snippet);
-GtkSourceSnippet            *gtk_source_completion_snippets_proposal_get_snippet 
(GtkSourceCompletionSnippetsProposal *self);
+struct _GtkSourceCompletionSnippetsProposal
+{
+       GObject                 parent_instance;
+  GtkSourceSnippetBundle *bundle;
+       GtkSourceSnippetInfo    info;
+};
+
+GtkSourceCompletionProposal *gtk_source_completion_snippets_proposal_new         (GtkSourceSnippetBundle     
         *bundle,
+                                                                                  const GtkSourceSnippetInfo 
         *info);
+GtkSourceSnippet            *gtk_source_completion_snippets_proposal_dup_snippet 
(GtkSourceCompletionSnippetsProposal *self);
 
 G_END_DECLS
diff --git a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal.c 
b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal.c
index 1bf69bbd..389364df 100644
--- a/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal.c
+++ b/gtksourceview/completion-providers/snippets/gtksourcecompletionsnippetsproposal.c
@@ -23,11 +23,6 @@
 
 #include "gtksourcecompletionsnippetsproposal-private.h"
 
-struct _GtkSourceCompletionSnippetsProposal
-{
-       GObject           parent_instance;
-       GtkSourceSnippet *snippet;
-};
 
 G_DEFINE_TYPE_WITH_CODE (GtkSourceCompletionSnippetsProposal,
                          gtk_source_completion_snippets_proposal,
@@ -43,16 +38,6 @@ enum {
 
 static GParamSpec *properties [N_PROPS];
 
-static void
-gtk_source_completion_snippets_proposal_finalize (GObject *object)
-{
-       GtkSourceCompletionSnippetsProposal *self = GTK_SOURCE_COMPLETION_SNIPPETS_PROPOSAL (object);
-
-       g_clear_object (&self->snippet);
-
-       G_OBJECT_CLASS (gtk_source_completion_snippets_proposal_parent_class)->finalize (object);
-}
-
 static void
 gtk_source_completion_snippets_proposal_get_property (GObject    *object,
                                                       guint       prop_id,
@@ -64,34 +49,11 @@ gtk_source_completion_snippets_proposal_get_property (GObject    *object,
        switch (prop_id)
        {
        case PROP_SNIPPET:
-               g_value_set_object (value, self->snippet);
+               g_value_take_object (value, gtk_source_completion_snippets_proposal_dup_snippet (self));
                break;
 
        case PROP_TRIGGER:
-               if (self->snippet != NULL)
-               {
-                       g_value_set_string (value, gtk_source_snippet_get_trigger (self->snippet));
-               }
-               break;
-
-       default:
-               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-               break;
-       }
-}
-
-static void
-gtk_source_completion_snippets_proposal_set_property (GObject      *object,
-                                                      guint         prop_id,
-                                                      const GValue *value,
-                                                      GParamSpec   *pspec)
-{
-       GtkSourceCompletionSnippetsProposal *self = GTK_SOURCE_COMPLETION_SNIPPETS_PROPOSAL (object);
-
-       switch (prop_id)
-       {
-       case PROP_SNIPPET:
-               self->snippet = g_value_dup_object (value);
+               g_value_set_string (value, self->info.trigger);
                break;
 
        default:
@@ -105,23 +67,15 @@ gtk_source_completion_snippets_proposal_class_init (GtkSourceCompletionSnippetsP
 {
        GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
-       object_class->finalize = gtk_source_completion_snippets_proposal_finalize;
        object_class->get_property = gtk_source_completion_snippets_proposal_get_property;
-       object_class->set_property = gtk_source_completion_snippets_proposal_set_property;
 
        properties [PROP_SNIPPET] =
-               g_param_spec_object ("snippet",
-                                    "snippet",
-                                    "The snippet to expand",
+               g_param_spec_object ("snippet", NULL, NULL,
                                     GTK_SOURCE_TYPE_SNIPPET,
-                                    (G_PARAM_READWRITE |
-                                     G_PARAM_CONSTRUCT_ONLY |
-                                     G_PARAM_STATIC_STRINGS));
+                                    (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
        properties [PROP_TRIGGER] =
-               g_param_spec_string ("trigger",
-                                    "Trigger",
-                                    "The trigger for the snippet",
+               g_param_spec_string ("trigger", NULL, NULL,
                                     NULL,
                                     (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
@@ -134,24 +88,22 @@ gtk_source_completion_snippets_proposal_init (GtkSourceCompletionSnippetsProposa
 }
 
 GtkSourceCompletionProposal *
-gtk_source_completion_snippets_proposal_new (GtkSourceSnippet *snippet)
+gtk_source_completion_snippets_proposal_new (GtkSourceSnippetBundle     *bundle,
+                                            const GtkSourceSnippetInfo *info)
 {
-       g_return_val_if_fail (GTK_SOURCE_IS_SNIPPET (snippet), NULL);
+       GtkSourceCompletionSnippetsProposal *ret;
+
+       g_return_val_if_fail (info != NULL, NULL);
+
+       ret = g_object_new (GTK_SOURCE_TYPE_COMPLETION_SNIPPETS_PROPOSAL, NULL);
+       g_set_object (&ret->bundle, bundle);
+       ret->info = *info;
 
-       return g_object_new (GTK_SOURCE_TYPE_COMPLETION_SNIPPETS_PROPOSAL,
-                            "snippet", snippet,
-                            NULL);
+       return GTK_SOURCE_COMPLETION_PROPOSAL (ret);
 }
 
-/**
- * gtk_source_completion_snippets_proposal_get_snippet:
- *
- * Returns: (transfer none): a #GtkSourceSnippet
- */
 GtkSourceSnippet *
-gtk_source_completion_snippets_proposal_get_snippet (GtkSourceCompletionSnippetsProposal *self)
+gtk_source_completion_snippets_proposal_dup_snippet (GtkSourceCompletionSnippetsProposal *self)
 {
-       g_return_val_if_fail (GTK_SOURCE_IS_COMPLETION_SNIPPETS_PROPOSAL (self), NULL);
-
-       return self->snippet;
+       return _gtk_source_snippet_bundle_create_snippet (self->bundle, &self->info);
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]