blob: aca42498abd691ebcd4b85e7ddc4ff9be3f11957 [file] [log] [blame]
/* GIO - GLib Input, Output and Streaming Library
*
* Copyright (C) 2006-2007 Red Hat, Inc.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General
* Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
*
* Author: Alexander Larsson <alexl@redhat.com>
*/
#include "config.h"
#include "gfilenamecompleter.h"
#include "gfileenumerator.h"
#include "gfileattribute.h"
#include "gfile.h"
#include "gfileinfo.h"
#include "gcancellable.h"
#include <string.h>
#include "glibintl.h"
/**
* GFilenameCompleter:
*
* Completes partial file and directory names given a partial string by
* looking in the file system for clues. Can return a list of possible
* completion strings for widget implementations.
*/
enum {
GOT_COMPLETION_DATA,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
typedef struct {
GFilenameCompleter *completer;
GFileEnumerator *enumerator;
GCancellable *cancellable;
gboolean should_escape;
GFile *dir;
GList *basenames;
gboolean dirs_only;
} LoadBasenamesData;
struct _GFilenameCompleter {
GObject parent;
GFile *basenames_dir;
gboolean basenames_are_escaped;
gboolean dirs_only;
GList *basenames;
LoadBasenamesData *basename_loader;
};
G_DEFINE_TYPE (GFilenameCompleter, g_filename_completer, G_TYPE_OBJECT)
static void cancel_load_basenames (GFilenameCompleter *completer);
static void
g_filename_completer_finalize (GObject *object)
{
GFilenameCompleter *completer;
completer = G_FILENAME_COMPLETER (object);
cancel_load_basenames (completer);
if (completer->basenames_dir)
g_object_unref (completer->basenames_dir);
g_list_free_full (completer->basenames, g_free);
G_OBJECT_CLASS (g_filename_completer_parent_class)->finalize (object);
}
static void
g_filename_completer_class_init (GFilenameCompleterClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = g_filename_completer_finalize;
/**
* GFilenameCompleter::got-completion-data:
*
* Emitted when the file name completion information comes available.
**/
signals[GOT_COMPLETION_DATA] = g_signal_new (I_("got-completion-data"),
G_TYPE_FILENAME_COMPLETER,
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GFilenameCompleterClass, got_completion_data),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
}
static void
g_filename_completer_init (GFilenameCompleter *completer)
{
}
/**
* g_filename_completer_new:
*
* Creates a new filename completer.
*
* Returns: a #GFilenameCompleter.
**/
GFilenameCompleter *
g_filename_completer_new (void)
{
return g_object_new (G_TYPE_FILENAME_COMPLETER, NULL);
}
static char *
longest_common_prefix (char *a, char *b)
{
char *start;
start = a;
while (g_utf8_get_char (a) == g_utf8_get_char (b))
{
a = g_utf8_next_char (a);
b = g_utf8_next_char (b);
}
return g_strndup (start, a - start);
}
static void
load_basenames_data_free (LoadBasenamesData *data)
{
if (data->enumerator)
g_object_unref (data->enumerator);
g_object_unref (data->cancellable);
g_object_unref (data->dir);
g_list_free_full (data->basenames, g_free);
g_free (data);
}
static void
got_more_files (GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
LoadBasenamesData *data = user_data;
GList *infos, *l;
GFileInfo *info;
const char *name;
gboolean append_slash;
char *t;
char *basename;
if (data->completer == NULL)
{
/* Was cancelled */
load_basenames_data_free (data);
return;
}
infos = g_file_enumerator_next_files_finish (data->enumerator, res, NULL);
for (l = infos; l != NULL; l = l->next)
{
info = l->data;
if (data->dirs_only &&
g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY)
{
g_object_unref (info);
continue;
}
append_slash = g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY;
name = g_file_info_get_name (info);
if (name == NULL)
{
g_object_unref (info);
continue;
}
if (data->should_escape)
basename = g_uri_escape_string (name,
G_URI_RESERVED_CHARS_ALLOWED_IN_PATH,
TRUE);
else
/* If not should_escape, must be a local filename, convert to utf8 */
basename = g_filename_to_utf8 (name, -1, NULL, NULL, NULL);
if (basename)
{
if (append_slash)
{
t = basename;
basename = g_strconcat (basename, "/", NULL);
g_free (t);
}
data->basenames = g_list_prepend (data->basenames, basename);
}
g_object_unref (info);
}
g_list_free (infos);
if (infos)
{
/* Not last, get more files */
g_file_enumerator_next_files_async (data->enumerator,
100,
0,
data->cancellable,
got_more_files, data);
}
else
{
data->completer->basename_loader = NULL;
if (data->completer->basenames_dir)
g_object_unref (data->completer->basenames_dir);
g_list_free_full (data->completer->basenames, g_free);
data->completer->basenames_dir = g_object_ref (data->dir);
data->completer->basenames = data->basenames;
data->completer->basenames_are_escaped = data->should_escape;
data->basenames = NULL;
g_file_enumerator_close_async (data->enumerator, 0, NULL, NULL, NULL);
g_signal_emit (data->completer, signals[GOT_COMPLETION_DATA], 0);
load_basenames_data_free (data);
}
}
static void
got_enum (GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
LoadBasenamesData *data = user_data;
if (data->completer == NULL)
{
/* Was cancelled */
load_basenames_data_free (data);
return;
}
data->enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, NULL);
if (data->enumerator == NULL)
{
data->completer->basename_loader = NULL;
if (data->completer->basenames_dir)
g_object_unref (data->completer->basenames_dir);
g_list_free_full (data->completer->basenames, g_free);
/* Mark up-to-date with no basenames */
data->completer->basenames_dir = g_object_ref (data->dir);
data->completer->basenames = NULL;
data->completer->basenames_are_escaped = data->should_escape;
load_basenames_data_free (data);
return;
}
g_file_enumerator_next_files_async (data->enumerator,
100,
0,
data->cancellable,
got_more_files, data);
}
static void
schedule_load_basenames (GFilenameCompleter *completer,
GFile *dir,
gboolean should_escape)
{
LoadBasenamesData *data;
cancel_load_basenames (completer);
data = g_new0 (LoadBasenamesData, 1);
data->completer = completer;
data->cancellable = g_cancellable_new ();
data->dir = g_object_ref (dir);
data->should_escape = should_escape;
data->dirs_only = completer->dirs_only;
completer->basename_loader = data;
g_file_enumerate_children_async (dir,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
0, 0,
data->cancellable,
got_enum, data);
}
static void
cancel_load_basenames (GFilenameCompleter *completer)
{
LoadBasenamesData *loader;
if (completer->basename_loader)
{
loader = completer->basename_loader;
loader->completer = NULL;
g_cancellable_cancel (loader->cancellable);
completer->basename_loader = NULL;
}
}
/* Returns a list of possible matches and the basename to use for it */
static GList *
init_completion (GFilenameCompleter *completer,
const char *initial_text,
char **basename_out)
{
gboolean should_escape;
GFile *file, *parent;
char *basename;
char *t;
int len;
*basename_out = NULL;
should_escape = ! (g_path_is_absolute (initial_text) || *initial_text == '~');
len = strlen (initial_text);
if (len > 0 &&
initial_text[len - 1] == '/')
return NULL;
file = g_file_parse_name (initial_text);
parent = g_file_get_parent (file);
if (parent == NULL)
{
g_object_unref (file);
return NULL;
}
if (completer->basenames_dir == NULL ||
completer->basenames_are_escaped != should_escape ||
!g_file_equal (parent, completer->basenames_dir))
{
schedule_load_basenames (completer, parent, should_escape);
g_object_unref (file);
return NULL;
}
basename = g_file_get_basename (file);
if (should_escape)
{
t = basename;
basename = g_uri_escape_string (basename, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE);
g_free (t);
}
else
{
t = basename;
basename = g_filename_to_utf8 (basename, -1, NULL, NULL, NULL);
g_free (t);
if (basename == NULL)
return NULL;
}
*basename_out = basename;
return completer->basenames;
}
/**
* g_filename_completer_get_completion_suffix:
* @completer: the filename completer.
* @initial_text: text to be completed.
*
* Obtains a completion for @initial_text from @completer.
*
* Returns: (nullable) (transfer full): a completed string, or %NULL if no
* completion exists. This string is not owned by GIO, so remember to g_free()
* it when finished.
**/
char *
g_filename_completer_get_completion_suffix (GFilenameCompleter *completer,
const char *initial_text)
{
GList *possible_matches, *l;
char *prefix;
char *suffix;
char *possible_match;
char *lcp;
g_return_val_if_fail (G_IS_FILENAME_COMPLETER (completer), NULL);
g_return_val_if_fail (initial_text != NULL, NULL);
possible_matches = init_completion (completer, initial_text, &prefix);
suffix = NULL;
for (l = possible_matches; l != NULL; l = l->next)
{
possible_match = l->data;
if (g_str_has_prefix (possible_match, prefix))
{
if (suffix == NULL)
suffix = g_strdup (possible_match + strlen (prefix));
else
{
lcp = longest_common_prefix (suffix,
possible_match + strlen (prefix));
g_free (suffix);
suffix = lcp;
if (*suffix == 0)
break;
}
}
}
g_free (prefix);
return suffix;
}
/**
* g_filename_completer_get_completions:
* @completer: the filename completer.
* @initial_text: text to be completed.
*
* Gets an array of completion strings for a given initial text.
*
* Returns: (array zero-terminated=1) (transfer full): array of strings with possible completions for @initial_text.
* This array must be freed by g_strfreev() when finished.
**/
char **
g_filename_completer_get_completions (GFilenameCompleter *completer,
const char *initial_text)
{
GList *possible_matches, *l;
char *prefix;
char *possible_match;
GPtrArray *res;
g_return_val_if_fail (G_IS_FILENAME_COMPLETER (completer), NULL);
g_return_val_if_fail (initial_text != NULL, NULL);
possible_matches = init_completion (completer, initial_text, &prefix);
res = g_ptr_array_new ();
for (l = possible_matches; l != NULL; l = l->next)
{
possible_match = l->data;
if (g_str_has_prefix (possible_match, prefix))
g_ptr_array_add (res,
g_strconcat (initial_text, possible_match + strlen (prefix), NULL));
}
g_free (prefix);
g_ptr_array_add (res, NULL);
return (char**)g_ptr_array_free (res, FALSE);
}
/**
* g_filename_completer_set_dirs_only:
* @completer: the filename completer.
* @dirs_only: a #gboolean.
*
* If @dirs_only is %TRUE, @completer will only
* complete directory names, and not file names.
**/
void
g_filename_completer_set_dirs_only (GFilenameCompleter *completer,
gboolean dirs_only)
{
g_return_if_fail (G_IS_FILENAME_COMPLETER (completer));
completer->dirs_only = dirs_only;
}