/* * Copyright (C) 2012-2024 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #ifdef HAVE_WINDOWS_H #define WIN32_LEAN_AND_MEAN #define UNICODE #include #endif #include #include #include "sciteco.h" #include "qreg.h" #include "interface.h" #include "string-utils.h" #include "file-utils.h" #ifdef G_OS_WIN32 /* * NOTE: File attributes are represented as DWORDs in the Win32 API * which should be equivalent to guint32. */ G_STATIC_ASSERT(sizeof(DWORD) == sizeof(teco_file_attributes_t)); /* * NOTE: Invalid file attributes should be represented by 0xFFFFFFFF. */ G_STATIC_ASSERT(INVALID_FILE_ATTRIBUTES == TECO_FILE_INVALID_ATTRIBUTES); teco_file_attributes_t teco_file_get_attributes(const gchar *filename) { g_autofree gunichar2 *filename_utf16 = g_utf8_to_utf16(filename, -1, NULL, NULL, NULL); return filename_utf16 ? GetFileAttributesW(filename_utf16) : TECO_FILE_INVALID_ATTRIBUTES; } void teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs) { g_autofree gunichar2 *filename_utf16 = g_utf8_to_utf16(filename, -1, NULL, NULL, NULL); if (filename_utf16) SetFileAttributesW(filename_utf16, attrs); } gchar * teco_file_get_absolute_path(const gchar *path) { if (!path) return NULL; g_autofree gunichar2 *path_utf16 = g_utf8_to_utf16(path, -1, NULL, NULL, NULL); TCHAR buf[MAX_PATH]; return path_utf16 && GetFullPathNameW(path_utf16, G_N_ELEMENTS(buf), buf, NULL) ? g_utf16_to_utf8(buf, -1, NULL, NULL, NULL) : NULL; } gboolean teco_file_is_visible(const gchar *path) { g_autofree gunichar2 *path_utf16 = g_utf8_to_utf16(path, -1, NULL, NULL, NULL); return path_utf16 && !(GetFileAttributesW(path_utf16) & FILE_ATTRIBUTE_HIDDEN); } #else /* !G_OS_WIN32 */ teco_file_attributes_t teco_file_get_attributes(const gchar *filename) { GStatBuf buf; return g_stat(filename, &buf) ? TECO_FILE_INVALID_ATTRIBUTES : buf.st_mode; } void teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs) { g_chmod(filename, attrs); } #ifdef G_OS_UNIX gchar * teco_file_get_absolute_path(const gchar *path) { gchar buf[PATH_MAX]; if (!path) return NULL; if (realpath(path, buf)) return g_strdup(buf); if (g_path_is_absolute(path)) return g_strdup(path); g_autofree gchar *cwd = g_get_current_dir(); return g_build_filename(cwd, path, NULL); } gboolean teco_file_is_visible(const gchar *path) { g_autofree gchar *basename = g_path_get_basename(path); return *basename != '.'; } #else /* !G_OS_UNIX */ #if GLIB_CHECK_VERSION(2,58,0) /* * FIXME: This should perhaps be preferred on any platform. * But it will complicate preprocessing. */ gchar * teco_file_get_absolute_path(const gchar *path) { return g_canonicalize_filename(path, NULL); } #else /* !GLIB_CHECK_VERSION(2,58,0) */ /* * This will never canonicalize relative paths. * I.e. the absolute path will often contain * relative components, even if `path` exists. * The only exception would be a simple filename * not containing any "..". */ gchar * teco_file_get_absolute_path(const gchar *path) { if (!path) return NULL; if (g_path_is_absolute(path)) return g_strdup(path); g_autofree gchar *cwd = g_get_current_dir(); return g_build_filename(cwd, path, NULL); } #endif /* !GLIB_CHECK_VERSION(2,58,0) */ /* * There's no platform-independent way to determine if a file * is visible/hidden, so we just assume that all files are * visible. */ gboolean teco_file_is_visible(const gchar *path) { return TRUE; } #endif /* !G_OS_UNIX */ #endif /* !G_OS_WIN32 */ /** * Perform tilde expansion on a file name or path. * * This supports only strings with a "~" prefix. * A user name after "~" is not supported. * The $HOME environment variable/register is used to retrieve * the current user's home directory. */ gchar * teco_file_expand_path(const gchar *path) { if (!path) return g_strdup(""); if (path[0] != '~' || (path[1] && !G_IS_DIR_SEPARATOR(path[1]))) return g_strdup(path); /* * $HOME should not have a trailing directory separator since * it is canonicalized to an absolute path at startup, * but this ensures that a proper path is constructed even if * it does (e.g. $HOME is changed later on). * * FIXME: In the future, it might be possible to remove the entire register. */ teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "$HOME", 5); g_assert(qreg != NULL); /* * Getting the string should not possible to fail. * The $HOME register should not contain any null-bytes on startup, * but it may have been changed later on. */ g_auto(teco_string_t) home = {NULL, 0}; if (!qreg->vtable->get_string(qreg, &home.data, &home.len, NULL, NULL) || teco_string_contains(&home, '\0')) return g_strdup(path); g_assert(home.data != NULL); return g_build_filename(home.data, path+1, NULL); } /** * Auto-complete a filename/directory. * * @param filename The filename to auto-complete or NULL. * @param file_test Restrict completion to files matching the test. * If G_FILE_TEST_EXISTS, both files and directories are completed. * If G_FILE_TEST_IS_DIR, only directories will be completed. * @param insert String to initialize with the autocompletion. * @return TRUE if the completion was unambiguous (eg. command can be terminated). */ gboolean teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_t *insert) { memset(insert, 0, sizeof(*insert)); g_autofree gchar *filename_expanded = teco_file_expand_path(filename); gsize filename_len = strlen(filename_expanded); /* * Derive base and directory names. * We do not use g_path_get_basename() or g_path_get_dirname() * since we need strict suffixes and prefixes of filename * in order to construct paths of entries in dirname * that are suitable for auto completion. */ gsize dirname_len = teco_file_get_dirname_len(filename_expanded); g_autofree gchar *dirname = g_strndup(filename_expanded, dirname_len); gchar *basename = filename_expanded + dirname_len; g_autoptr(GDir) dir = g_dir_open(dirname_len ? dirname : ".", 0, NULL); if (!dir) return FALSE; /* * On Windows, both forward and backslash * directory separators are allowed in directory * names passed to glib. * To imitate glib's behaviour, we use * the last valid directory separator in `filename_expanded` * to generate new separators. * This also allows forward-slash auto-completion * on Windows. */ const gchar *dir_sep = dirname_len ? dirname + dirname_len - 1 : G_DIR_SEPARATOR_S; GSList *files = NULL; guint files_len = 0; gsize prefix_len = 0; const gchar *cur_basename; while ((cur_basename = g_dir_read_name(dir))) { if (!g_str_has_prefix(cur_basename, basename)) continue; /* * NOTE: `dirname` contains any directory separator, so strcat() works here. * Reserving one byte at the end of the filename ensures we can easily * append the directory separator without reallocations. */ gchar *cur_filename = g_malloc(strlen(dirname)+strlen(cur_basename)+2); strcat(strcpy(cur_filename, dirname), cur_basename); /* * NOTE: This avoids g_file_test() for G_FILE_TEST_EXISTS * since the file we process here should always exist. */ if ((!*basename && !teco_file_is_visible(cur_filename)) || (file_test != G_FILE_TEST_EXISTS && !g_file_test(cur_filename, file_test))) { g_free(cur_filename); continue; } if (file_test == G_FILE_TEST_IS_DIR || g_file_test(cur_filename, G_FILE_TEST_IS_DIR)) strcat(cur_filename, dir_sep); files = g_slist_prepend(files, cur_filename); if (g_slist_next(files)) { teco_string_t other_file; other_file.data = (gchar *)g_slist_next(files)->data + filename_len; other_file.len = strlen(other_file.data); gsize len = teco_string_diff(&other_file, cur_filename + filename_len, strlen(cur_filename) - filename_len); if (len < prefix_len) prefix_len = len; } else { prefix_len = strlen(cur_filename + filename_len); } files_len++; } if (prefix_len > 0) { teco_string_init(insert, (gchar *)files->data + filename_len, prefix_len); } else if (files_len > 1) { files = g_slist_sort(files, (GCompareFunc)g_strcmp0); for (GSList *file = files; file; file = g_slist_next(file)) { teco_popup_entry_type_t type = TECO_POPUP_DIRECTORY; gboolean is_buffer = FALSE; if (!teco_file_is_dir((gchar *)file->data)) { type = TECO_POPUP_FILE; /* FIXME: inefficient */ is_buffer = teco_ring_find((gchar *)file->data) != NULL; } teco_interface_popup_add(type, (gchar *)file->data, strlen((gchar *)file->data), is_buffer); } teco_interface_popup_show(); } /* * FIXME: If we are completing only directories, * we can theoretically insert the completed character * after directories without subdirectories. */ gboolean unambiguous = files_len == 1 && !teco_file_is_dir((gchar *)files->data); g_slist_free_full(files, g_free); return unambiguous; }