Branch data Line data Source code
1 : : /* GIO - GLib Input, Output and Streaming Library
2 : : *
3 : : * Copyright (C) 2006-2007 Red Hat, Inc.
4 : : *
5 : : * SPDX-License-Identifier: LGPL-2.1-or-later
6 : : *
7 : : * This library is free software; you can redistribute it and/or
8 : : * modify it under the terms of the GNU Lesser General Public
9 : : * License as published by the Free Software Foundation; either
10 : : * version 2.1 of the License, or (at your option) any later version.
11 : : *
12 : : * This library is distributed in the hope that it will be useful,
13 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 : : * Lesser General Public License for more details.
16 : : *
17 : : * You should have received a copy of the GNU Lesser General
18 : : * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
19 : : *
20 : : * Author: Alexander Larsson <alexl@redhat.com>
21 : : */
22 : :
23 : : #include "config.h"
24 : : #include "gfilenamecompleter.h"
25 : : #include "gfileenumerator.h"
26 : : #include "gfileattribute.h"
27 : : #include "gfile.h"
28 : : #include "gfileinfo.h"
29 : : #include "gcancellable.h"
30 : : #include <string.h>
31 : : #include "glibintl.h"
32 : :
33 : :
34 : : /**
35 : : * GFilenameCompleter:
36 : : *
37 : : * Completes partial file and directory names given a partial string by
38 : : * looking in the file system for clues. Can return a list of possible
39 : : * completion strings for widget implementations.
40 : : */
41 : :
42 : : enum {
43 : : GOT_COMPLETION_DATA,
44 : : LAST_SIGNAL
45 : : };
46 : :
47 : : static guint signals[LAST_SIGNAL] = { 0 };
48 : :
49 : : typedef struct {
50 : : GFilenameCompleter *completer;
51 : : GFileEnumerator *enumerator;
52 : : GCancellable *cancellable;
53 : : gboolean should_escape;
54 : : GFile *dir;
55 : : GList *basenames;
56 : : gboolean dirs_only;
57 : : } LoadBasenamesData;
58 : :
59 : : struct _GFilenameCompleter {
60 : : GObject parent;
61 : :
62 : : GFile *basenames_dir;
63 : : gboolean basenames_are_escaped;
64 : : gboolean dirs_only;
65 : : GList *basenames;
66 : :
67 : : LoadBasenamesData *basename_loader;
68 : : };
69 : :
70 : 48 : G_DEFINE_TYPE (GFilenameCompleter, g_filename_completer, G_TYPE_OBJECT)
71 : :
72 : : static void cancel_load_basenames (GFilenameCompleter *completer);
73 : :
74 : : static void
75 : 7 : g_filename_completer_finalize (GObject *object)
76 : : {
77 : : GFilenameCompleter *completer;
78 : :
79 : 7 : completer = G_FILENAME_COMPLETER (object);
80 : :
81 : 7 : cancel_load_basenames (completer);
82 : :
83 : 7 : if (completer->basenames_dir)
84 : 6 : g_object_unref (completer->basenames_dir);
85 : :
86 : 7 : g_list_free_full (completer->basenames, g_free);
87 : :
88 : 7 : G_OBJECT_CLASS (g_filename_completer_parent_class)->finalize (object);
89 : 7 : }
90 : :
91 : : static void
92 : 3 : g_filename_completer_class_init (GFilenameCompleterClass *klass)
93 : : {
94 : 3 : GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
95 : :
96 : 3 : gobject_class->finalize = g_filename_completer_finalize;
97 : : /**
98 : : * GFilenameCompleter::got-completion-data:
99 : : *
100 : : * Emitted when the file name completion information comes available.
101 : : **/
102 : 3 : signals[GOT_COMPLETION_DATA] = g_signal_new (I_("got-completion-data"),
103 : : G_TYPE_FILENAME_COMPLETER,
104 : : G_SIGNAL_RUN_LAST,
105 : : G_STRUCT_OFFSET (GFilenameCompleterClass, got_completion_data),
106 : : NULL, NULL,
107 : : NULL,
108 : : G_TYPE_NONE, 0);
109 : 3 : }
110 : :
111 : : static void
112 : 7 : g_filename_completer_init (GFilenameCompleter *completer)
113 : : {
114 : 7 : }
115 : :
116 : : /**
117 : : * g_filename_completer_new:
118 : : *
119 : : * Creates a new filename completer.
120 : : *
121 : : * Returns: a #GFilenameCompleter.
122 : : **/
123 : : GFilenameCompleter *
124 : 6 : g_filename_completer_new (void)
125 : : {
126 : 6 : return g_object_new (G_TYPE_FILENAME_COMPLETER, NULL);
127 : : }
128 : :
129 : : static char *
130 : 6 : longest_common_prefix (char *a, char *b)
131 : : {
132 : : char *start;
133 : :
134 : 6 : start = a;
135 : :
136 : 29 : while (g_utf8_get_char (a) == g_utf8_get_char (b))
137 : : {
138 : 23 : a = g_utf8_next_char (a);
139 : 23 : b = g_utf8_next_char (b);
140 : : }
141 : :
142 : 6 : return g_strndup (start, a - start);
143 : : }
144 : :
145 : : static void
146 : 6 : load_basenames_data_free (LoadBasenamesData *data)
147 : : {
148 : 6 : if (data->enumerator)
149 : 6 : g_object_unref (data->enumerator);
150 : :
151 : 6 : g_object_unref (data->cancellable);
152 : 6 : g_object_unref (data->dir);
153 : :
154 : 6 : g_list_free_full (data->basenames, g_free);
155 : :
156 : 6 : g_free (data);
157 : 6 : }
158 : :
159 : : static void
160 : 12 : got_more_files (GObject *source_object,
161 : : GAsyncResult *res,
162 : : gpointer user_data)
163 : : {
164 : 12 : LoadBasenamesData *data = user_data;
165 : : GList *infos, *l;
166 : : GFileInfo *info;
167 : : const char *name;
168 : : gboolean append_slash;
169 : : char *t;
170 : : char *basename;
171 : :
172 : 12 : if (data->completer == NULL)
173 : : {
174 : : /* Was cancelled */
175 : 0 : load_basenames_data_free (data);
176 : 0 : return;
177 : : }
178 : :
179 : 12 : infos = g_file_enumerator_next_files_finish (data->enumerator, res, NULL);
180 : :
181 : 36 : for (l = infos; l != NULL; l = l->next)
182 : : {
183 : 24 : info = l->data;
184 : :
185 : 36 : if (data->dirs_only &&
186 : 12 : g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY)
187 : : {
188 : 6 : g_object_unref (info);
189 : 6 : continue;
190 : : }
191 : :
192 : 18 : append_slash = g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY;
193 : 18 : name = g_file_info_get_name (info);
194 : 18 : if (name == NULL)
195 : : {
196 : 0 : g_object_unref (info);
197 : 0 : continue;
198 : : }
199 : :
200 : :
201 : 18 : if (data->should_escape)
202 : 0 : basename = g_uri_escape_string (name,
203 : : G_URI_RESERVED_CHARS_ALLOWED_IN_PATH,
204 : : TRUE);
205 : : else
206 : : /* If not should_escape, must be a local filename, convert to utf8 */
207 : 18 : basename = g_filename_to_utf8 (name, -1, NULL, NULL, NULL);
208 : :
209 : 18 : if (basename)
210 : : {
211 : 18 : if (append_slash)
212 : : {
213 : 12 : t = basename;
214 : 12 : basename = g_strconcat (basename, G_DIR_SEPARATOR_S, NULL);
215 : 12 : g_free (t);
216 : : }
217 : :
218 : 18 : data->basenames = g_list_prepend (data->basenames, basename);
219 : : }
220 : :
221 : 18 : g_object_unref (info);
222 : : }
223 : :
224 : 12 : g_list_free (infos);
225 : :
226 : 12 : if (infos)
227 : : {
228 : : /* Not last, get more files */
229 : 6 : g_file_enumerator_next_files_async (data->enumerator,
230 : : 100,
231 : : 0,
232 : : data->cancellable,
233 : : got_more_files, data);
234 : : }
235 : : else
236 : : {
237 : 6 : data->completer->basename_loader = NULL;
238 : :
239 : 6 : if (data->completer->basenames_dir)
240 : 0 : g_object_unref (data->completer->basenames_dir);
241 : 6 : g_list_free_full (data->completer->basenames, g_free);
242 : :
243 : 6 : data->completer->basenames_dir = g_object_ref (data->dir);
244 : 6 : data->completer->basenames = data->basenames;
245 : 6 : data->completer->basenames_are_escaped = data->should_escape;
246 : 6 : data->basenames = NULL;
247 : :
248 : 6 : g_file_enumerator_close_async (data->enumerator, 0, NULL, NULL, NULL);
249 : :
250 : 6 : g_signal_emit (data->completer, signals[GOT_COMPLETION_DATA], 0);
251 : 6 : load_basenames_data_free (data);
252 : : }
253 : : }
254 : :
255 : :
256 : : static void
257 : 6 : got_enum (GObject *source_object,
258 : : GAsyncResult *res,
259 : : gpointer user_data)
260 : : {
261 : 6 : LoadBasenamesData *data = user_data;
262 : :
263 : 6 : if (data->completer == NULL)
264 : : {
265 : : /* Was cancelled */
266 : 0 : load_basenames_data_free (data);
267 : 0 : return;
268 : : }
269 : :
270 : 6 : data->enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, NULL);
271 : :
272 : 6 : if (data->enumerator == NULL)
273 : : {
274 : 0 : data->completer->basename_loader = NULL;
275 : :
276 : 0 : if (data->completer->basenames_dir)
277 : 0 : g_object_unref (data->completer->basenames_dir);
278 : 0 : g_list_free_full (data->completer->basenames, g_free);
279 : :
280 : : /* Mark up-to-date with no basenames */
281 : 0 : data->completer->basenames_dir = g_object_ref (data->dir);
282 : 0 : data->completer->basenames = NULL;
283 : 0 : data->completer->basenames_are_escaped = data->should_escape;
284 : :
285 : 0 : load_basenames_data_free (data);
286 : 0 : return;
287 : : }
288 : :
289 : 6 : g_file_enumerator_next_files_async (data->enumerator,
290 : : 100,
291 : : 0,
292 : : data->cancellable,
293 : : got_more_files, data);
294 : : }
295 : :
296 : : static void
297 : 6 : schedule_load_basenames (GFilenameCompleter *completer,
298 : : GFile *dir,
299 : : gboolean should_escape)
300 : : {
301 : : LoadBasenamesData *data;
302 : :
303 : 6 : cancel_load_basenames (completer);
304 : :
305 : 6 : data = g_new0 (LoadBasenamesData, 1);
306 : 6 : data->completer = completer;
307 : 6 : data->cancellable = g_cancellable_new ();
308 : 6 : data->dir = g_object_ref (dir);
309 : 6 : data->should_escape = should_escape;
310 : 6 : data->dirs_only = completer->dirs_only;
311 : :
312 : 6 : completer->basename_loader = data;
313 : :
314 : 6 : g_file_enumerate_children_async (dir,
315 : : G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
316 : : 0, 0,
317 : : data->cancellable,
318 : : got_enum, data);
319 : 6 : }
320 : :
321 : : static void
322 : 13 : cancel_load_basenames (GFilenameCompleter *completer)
323 : : {
324 : : LoadBasenamesData *loader;
325 : :
326 : 13 : if (completer->basename_loader)
327 : : {
328 : 0 : loader = completer->basename_loader;
329 : 0 : loader->completer = NULL;
330 : :
331 : 0 : g_cancellable_cancel (loader->cancellable);
332 : :
333 : 0 : completer->basename_loader = NULL;
334 : : }
335 : 13 : }
336 : :
337 : :
338 : : /* Returns a list of possible matches and the basename to use for it */
339 : : static GList *
340 : 18 : init_completion (GFilenameCompleter *completer,
341 : : const char *initial_text,
342 : : char **basename_out)
343 : : {
344 : : gboolean should_escape;
345 : 18 : GFile *file = NULL, *parent = NULL;
346 : : char *basename;
347 : : char *t;
348 : : size_t len;
349 : : GList *basenames;
350 : :
351 : 18 : basenames = NULL;
352 : 18 : *basename_out = NULL;
353 : :
354 : 18 : should_escape = ! (g_path_is_absolute (initial_text) || *initial_text == '~');
355 : :
356 : 18 : len = strlen (initial_text);
357 : :
358 : 18 : if (len > 0 &&
359 : 18 : initial_text[len - 1] == '/')
360 : 0 : goto out;
361 : :
362 : 18 : file = g_file_parse_name (initial_text);
363 : 18 : parent = g_file_get_parent (file);
364 : 18 : if (parent == NULL)
365 : 0 : goto out;
366 : :
367 : 18 : if (completer->basenames_dir == NULL ||
368 : 24 : completer->basenames_are_escaped != should_escape ||
369 : 12 : !g_file_equal (parent, completer->basenames_dir))
370 : : {
371 : 6 : schedule_load_basenames (completer, parent, should_escape);
372 : :
373 : 6 : goto out;
374 : : }
375 : :
376 : 12 : basename = g_file_get_basename (file);
377 : 12 : if (should_escape)
378 : : {
379 : 0 : t = basename;
380 : 0 : basename = g_uri_escape_string (basename, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE);
381 : 0 : g_free (t);
382 : : }
383 : : else
384 : : {
385 : 12 : t = basename;
386 : 12 : basename = g_filename_to_utf8 (basename, -1, NULL, NULL, NULL);
387 : 12 : g_free (t);
388 : :
389 : 12 : if (basename == NULL)
390 : 0 : goto out;
391 : : }
392 : :
393 : 12 : basenames = completer->basenames;
394 : 12 : *basename_out = basename;
395 : :
396 : 18 : out:
397 : 18 : g_clear_object (&file);
398 : 18 : g_clear_object (&parent);
399 : :
400 : 18 : return basenames;
401 : : }
402 : :
403 : : /**
404 : : * g_filename_completer_get_completion_suffix:
405 : : * @completer: the filename completer.
406 : : * @initial_text: text to be completed.
407 : : *
408 : : * Obtains a suffix completion for @initial_text from @completer.
409 : : *
410 : : * Suffix will be an empty string if there's no shared suffix among matching
411 : : * completions. If there's no matching completions anyway, `NULL` is returned.
412 : : *
413 : : * Returns: (nullable) (transfer full): a suffix completion string, or `NULL` if no
414 : : * completion exists.
415 : : **/
416 : : char *
417 : 6 : g_filename_completer_get_completion_suffix (GFilenameCompleter *completer,
418 : : const char *initial_text)
419 : : {
420 : : GList *possible_matches, *l;
421 : : char *prefix;
422 : : char *suffix;
423 : : char *possible_match;
424 : : char *lcp;
425 : :
426 : 6 : g_return_val_if_fail (G_IS_FILENAME_COMPLETER (completer), NULL);
427 : 6 : g_return_val_if_fail (initial_text != NULL, NULL);
428 : :
429 : 6 : possible_matches = init_completion (completer, initial_text, &prefix);
430 : :
431 : 6 : suffix = NULL;
432 : :
433 : 22 : for (l = possible_matches; l != NULL; l = l->next)
434 : : {
435 : 17 : possible_match = l->data;
436 : :
437 : 17 : if (g_str_has_prefix (possible_match, prefix))
438 : : {
439 : 11 : if (suffix == NULL)
440 : 10 : suffix = g_strdup (possible_match + strlen (prefix));
441 : : else
442 : : {
443 : 6 : lcp = longest_common_prefix (suffix,
444 : 6 : possible_match + strlen (prefix));
445 : 6 : g_free (suffix);
446 : 6 : suffix = lcp;
447 : :
448 : 6 : if (*suffix == 0)
449 : 1 : break;
450 : : }
451 : : }
452 : : }
453 : :
454 : 6 : g_free (prefix);
455 : :
456 : 6 : return suffix;
457 : : }
458 : :
459 : : /**
460 : : * g_filename_completer_get_completions:
461 : : * @completer: the filename completer.
462 : : * @initial_text: text to be completed.
463 : : *
464 : : * Gets an array of completion strings for a given initial text.
465 : : *
466 : : * Returns: (array zero-terminated=1) (transfer full): array of strings with possible completions for @initial_text.
467 : : * This array must be freed by g_strfreev() when finished.
468 : : **/
469 : : char **
470 : 12 : g_filename_completer_get_completions (GFilenameCompleter *completer,
471 : : const char *initial_text)
472 : : {
473 : : GList *possible_matches, *l;
474 : : char *prefix;
475 : : char *possible_match;
476 : : GPtrArray *res;
477 : :
478 : 12 : g_return_val_if_fail (G_IS_FILENAME_COMPLETER (completer), NULL);
479 : 12 : g_return_val_if_fail (initial_text != NULL, NULL);
480 : :
481 : 12 : possible_matches = init_completion (completer, initial_text, &prefix);
482 : :
483 : 12 : res = g_ptr_array_new ();
484 : 30 : for (l = possible_matches; l != NULL; l = l->next)
485 : : {
486 : 18 : possible_match = l->data;
487 : :
488 : 18 : if (g_str_has_prefix (possible_match, prefix))
489 : 12 : g_ptr_array_add (res,
490 : 12 : g_strconcat (initial_text, possible_match + strlen (prefix), NULL));
491 : : }
492 : :
493 : 12 : g_free (prefix);
494 : :
495 : 12 : g_ptr_array_add (res, NULL);
496 : :
497 : 12 : return (char**)g_ptr_array_free (res, FALSE);
498 : : }
499 : :
500 : : /**
501 : : * g_filename_completer_set_dirs_only:
502 : : * @completer: the filename completer.
503 : : * @dirs_only: a #gboolean.
504 : : *
505 : : * If @dirs_only is %TRUE, @completer will only
506 : : * complete directory names, and not file names.
507 : : *
508 : : * This function needs to be called before waiting for results from the
509 : : * completer to be populated.
510 : : **/
511 : : void
512 : 6 : g_filename_completer_set_dirs_only (GFilenameCompleter *completer,
513 : : gboolean dirs_only)
514 : : {
515 : 6 : g_return_if_fail (G_IS_FILENAME_COMPLETER (completer));
516 : :
517 : 6 : completer->dirs_only = dirs_only;
518 : : }
|