| /* 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; |
| } |